diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index c0e9ca5f571b8..35efbb97a0a03 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -126,7 +126,7 @@ Since Elasticsearch has a default limit of 1000 fields per index, plugins should fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the .kibana index. - ## References +## References Declare by adding an id, type and name to the `references` array. @@ -159,12 +159,14 @@ identify this reference. This guarantees that the id the reference points to alw visualization id was directly stored in `dashboard.panels[0].visualization` there is a risk that this id gets updated without updating the reference in the references array. -## Writing migrations +## Migrations Saved Objects support schema changes between Kibana versions, which we call migrations. Migrations are - applied when a Kibana installation is upgraded from one version to the next, when exports are imported via + applied when a Kibana installation is upgraded from one version to a newer version, when exports are imported via the Saved Objects Management UI, or when a new object is created via the HTTP API. +### Writing migrations + Each Saved Object type may define migrations for its schema. Migrations are specified by the Kibana version number, receive an input document, and must return the fully migrated document to be persisted to Elasticsearch. @@ -245,10 +247,11 @@ export const dashboardVisualization: SavedObjectsType = { in which this migration was released. So if you are creating a migration which will be part of the v7.10.0 release, but will also be backported and released as v7.9.3, the migration version should be: 7.9.3. - Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. - Having said that, if a - document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to - fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. +Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. +Having said that, if a document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to +fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered, +the error should be verbose and informative so that the corrupt document can be corrected, if possible. + +### Testing Migrations -It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch -conditions in a migration function and the high impact of a bug in this code, there’s really no reason not to aim for 100% test code coverage. +Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the . diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index 96e13555a36a2..55b662421cbd0 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -569,7 +569,7 @@ describe('renderApp', () => { }); ``` -### SavedObjects +### SavedObjectsClient #### Unit Tests @@ -794,6 +794,192 @@ Kibana and esArchiver to load fixture data into Elasticsearch. _todo: fully worked out example_ +### Saved Objects migrations + +_Also see ._ + +It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input +documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a +bug in this code, there’s really no reason not to aim for 100% test code coverage. + +It's recommend that you primarily leverage unit testing with Jest for testing your migration function. Unit tests will +be a much more effective approach to testing all the different shapes of input data and edge cases that your migration +may need to handle. With more complex migrations that interact with several components or may behave different depending +on registry contents (such as Embeddable migrations), we recommend that you use the Jest Integration suite which allows +you to create a full instance Kibana and all plugins in memory and leverage the import API to test migrating documents. + +#### Throwing exceptions +Keep in mind that any exception thrown by your migration function will cause Kibana to fail to upgrade. This should almost +never happen for our end users and we should be exhaustive in our testing to be sure to catch as many edge cases that we +could possibly handle. This entails ensuring that the migration is written defensively; we should try to avoid every bug +possible in our implementation. + +In general, exceptions should only be thrown when the input data is corrupted and doesn't match the expected schema. In +such cases, it's important that an informative error message is included in the exception and we do not rely on implicit +runtime exceptions such as "null pointer exceptions" like `TypeError: Cannot read property 'foo' of undefined`. + +#### Unit testing + +Unit testing migration functions is typically pretty straight forward and comparable to other types of Jest testing. In +general, you should focus this tier of testing on validating output and testing input edge cases. One focus of this tier +should be trying to find edge cases that throw exceptions the migration shouldn't. As you can see in this simple +example, the coverage here is very exhaustive and verbose, which is intentional. + +```ts +import { migrateCaseFromV7_9_0ToV7_10_0 } from './case_migrations'; + +const validInput_7_9_0 = { + id: '1', + type: 'case', + attributes: { + connector_id: '1234'; + } +} + +describe('Case migrations v7.7.0 -> v7.8.0', () => { + it('transforms the connector field', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0(validInput_7_9_0)).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: '1234', // verify id was moved into subobject + name: 'none', // verify new default field was added + } + } + }); + }); + + it('handles empty string', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: '' + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles null', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: null + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles undefined', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // Even though undefined isn't a valid JSON or Elasticsearch value, we should test it anyways since there + // could be some JavaScript layer that casts the field to `undefined` for some reason. + connector_id: undefined + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // also test without the field present at all + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); +}); +``` + +#### Integration testing +With more complicated migrations, the behavior of the migration may be dependent on values from other plugins which may +be difficult or even impossible to test with unit tests. You need to actually bootstrap Kibana, load the plugins, and +then test the full end-to-end migration. This type of set up will also test ingesting your documents into Elasticsearch +against the mappings defined by your Saved Object type. + +This can be achieved using the `jest_integration` suite and the `kbnTestServer` utility for starting an in-memory +instance of Kibana. You can then leverage the import API to test migrations. This API applies the same migrations to +imported documents as are applied at Kibana startup and is much easier to work with for testing. + +```ts +// You may need to adjust these paths depending on where your test file is located. +// The absolute path is src/core/test_helpers/so_migrations +import { createTestHarness, SavedObjectTestHarness } from '../../../../src/core/test_helpers/so_migrations'; + +describe('my plugin migrations', () => { + let testHarness: SavedObjectTestHarness; + + beforeAll(async () => { + testHarness = createTestHarness(); + await testHarness.start(); + }); + + afterAll(async () => { + await testHarness.stop(); + }); + + it('successfully migrates valid case documents', async () => { + expect( + await testHarness.migrate([ + { type: 'case', id: '1', attributes: { connector_id: '1234' }, references: [] }, + { type: 'case', id: '2', attributes: { connector_id: '' }, references: [] }, + { type: 'case', id: '3', attributes: { connector_id: null }, references: [] }, + ]) + ).toEqual([ + expect.objectContaining( + { type: 'case', id: '1', attributes: { connector: { id: '1234', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '2', attributes: { connector: { id: 'none', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '3', attributes: { connector: { id: 'none', name: 'none' } } }), + ]) + }) +}) +``` + +There are some caveats about using the import/export API for testing migrations: +- You cannot test the startup behavior of Kibana this way. This should not have any effect on type migrations but does + mean that this method cannot be used for testing the migration algorithm itself. +- While not yet supported, if support is added for migrations that affect multiple types, it's possible that the + behavior during import may vary slightly from the upgrade behavior. + ### Elasticsearch _How to test ES clients_ diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index b2ce650a531dc..96a7e57ef3e4f 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -18,8 +18,6 @@ This section summarizes the changes in each release. [[release-notes-8.0.0-alpha1]] == {kib} 8.0.0-alpha1 -coming[8.0.0] - The following changes are released for the first time in {kib} 8.0.0-alpha1. Review the changes, then use the <> to complete the upgrade. [float] diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index 0218bac58815a..c7b12c4ac32f6 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,9 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +TIP: When you upload GeoJSON or delimited files in {kib}, there is a file size +limit, which is configurable in <>. + [discrete] [[import-geospatial-privileges]] === Security privileges @@ -18,37 +21,36 @@ spaces in **{stack-manage-app}** in {kib}. For more information, see To upload GeoJSON files in {kib} with *Maps*, you must have: -* The `all` {kib} privilege for *Maps*. -* The `all` {kib} privilege for *Index Pattern Management*. -* The `create` and `create_index` index privileges for destination indices. -* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. +* The `all` {kib} privilege for *Maps* +* The `all` {kib} privilege for *{ipm-app}* +* The `create` and `create_index` index privileges for destination indices +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices -To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: +To upload delimited files (such as CSV, TSV, or JSON files) on the {kib} home page, you must also have: -* The `manage_pipeline` cluster privilege. -* The `read` {kib} privilege for *Machine Learning*. -* The `machine_learning_admin` or `machine_learning_user` role. +* The `all` {kib} privilege for *Discover* +* The `manage_pipeline` or `manage_ingest_pipelines` cluster privilege +* The `manage` index privilege for destination indices [discrete] -=== Upload CSV with latitude and longitude columns +=== Upload delimited files with latitude and longitude columns -*File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point. +On the {kib} home page, you can upload a file and import it into an {es} index with latitude and longitude columns combined into a `geo_point` field. -. Open the main menu, then click *Machine Learning*. -. Select the *Data Visualizer* tab, then click *Upload file*. -. Use the file chooser to select a CSV file. +. Go to the {kib} home page and click *Upload a file*. +. Select a file in one of the supported file formats. . Click *Import*. . Select the *Advanced* tab. . Set *Index name*. -. Click *Add combined field*, then click *Add geo point field*. +. If a combined `geo_point` field is not created automatically, click *Add combined field*, then click *Add geo point field*. . Fill out the form and click *Add*. . Click *Import*. [discrete] === Upload a GeoJSON file -*Upload GeoJSON* indexes GeoJSON features as a geo_point or geo_shape. +*Upload GeoJSON* indexes GeoJSON features as a `geo_point` or `geo_shape`. . <>. . Click *Add layer*. diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index e08695b334e1b..734e26c5199d7 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,6 +22,11 @@ interface UrlParam { username?: string; } +interface App { + pathname?: string; + hash?: string; +} + /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -41,11 +46,11 @@ interface UrlParam { * @return {string} */ -function getUrl(config: UrlParam, app: UrlParam) { +function getUrl(config: UrlParam, app: App) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 8bda77563be8c..f473b3ed02526 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -43,6 +43,8 @@ export const REMOVED_TYPES: string[] = [ 'server', // https://github.com/elastic/kibana/issues/95617 'tsvb-validation-telemetry', + // replaced by osquery-manager-usage-metric + 'osquery-usage-metric', ].sort(); // When migrating from the outdated index we use a read query which excludes diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts index 0bad209ad9cef..ce4c8078e0c95 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts @@ -72,6 +72,7 @@ const previouslyRegisteredTypes = [ 'monitoring-telemetry', 'osquery-saved-query', 'osquery-usage-metric', + 'osquery-manager-usage-metric', 'query', 'sample-data-telemetry', 'search', diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index e137955674457..96888a07be68f 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -10,13 +10,16 @@ import { DiscoverServices } from '../build_services'; import { dataPluginMock } from '../../../data/public/mocks'; import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { + CONTEXT_STEP_SETTING, DEFAULT_COLUMNS_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../common'; import { savedSearchMock } from './saved_search'; import { UI_SETTINGS } from '../../../data/common'; import { TopNavMenu } from '../../../navigation/public'; +import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; const dataPlugin = dataPluginMock.createStartContract(); export const discoverServiceMock = ({ @@ -49,10 +52,16 @@ export const discoverServiceMock = ({ return []; } else if (key === UI_SETTINGS.META_FIELDS) { return []; - } else if (key === SAMPLE_SIZE_SETTING) { - return 250; + } else if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } else if (key === CONTEXT_STEP_SETTING) { + return 5; } else if (key === SORT_DEFAULT_ORDER_SETTING) { return 'desc'; + } else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) { + return false; + } else if (key === SAMPLE_SIZE_SETTING) { + return 250; } }, isDefault: (key: string) => { diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 727f941f5fee3..e9da3a7c4784f 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -22,10 +22,7 @@ export enum SurrDocType { } export type EsHitRecord = Required< - Pick< - estypes.SearchResponse['hits']['hits'][number], - '_id' | 'fields' | 'sort' | '_index' | '_version' - > + Pick > & { _source?: Record; _score?: number; diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 810be94ce24b0..b79936bd6f385 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -7,25 +7,40 @@ */ import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { getServices } from '../../kibana_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { TotalDocuments } from '../apps/main/components/total_documents/total_documents'; + +export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { + totalHitCount: number; +} export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( )); -export function DiscoverGridEmbeddable(props: DiscoverGridProps) { +export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); return ( - + + {props.totalHitCount !== 0 && ( + + + + )} + + + + ); } diff --git a/src/plugins/discover/public/application/angular/directives/render_complete.ts b/src/plugins/discover/public/application/angular/directives/render_complete.ts deleted file mode 100644 index b62820bf876d8..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/render_complete.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IScope } from 'angular'; -import { RenderCompleteListener } from '../../../../../kibana_utils/public'; - -export function createRenderCompleteDirective() { - return { - controller($scope: IScope, $element: JQLite) { - const el = $element[0]; - const renderCompleteListener = new RenderCompleteListener(el); - $scope.$on('$destroy', renderCompleteListener.destroy); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap deleted file mode 100644 index fd0c1b4b2af8d..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerButtons 1`] = ` - - - - - - - - -`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap deleted file mode 100644 index 96d2994bbe68f..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerText without crashing 1`] = ` -
- 1–2 of 3 -
-`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts deleted file mode 100644 index 180da83beb2af..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ToolBarPagerText } from './tool_bar_pager_text'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerTextDirective(reactDirective: any) { - return reactDirective(ToolBarPagerText); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerButtonsDirective(reactDirective: any) { - return reactDirective(ToolBarPagerButtons); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx deleted file mode 100644 index 061dae2d50658..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ /dev/null @@ -1,51 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { findTestSubject } from '@elastic/eui/lib/test'; - -test('it renders ToolBarPagerButtons', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ToolBarPagerButtons with clickable next and previous button', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(1); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(1); -}); - -test('it renders ToolBarPagerButtons with disabled next and previous button', () => { - const props = { - hasPreviousPage: false, - hasNextPage: false, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(0); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(0); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx deleted file mode 100644 index d825220163165..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -interface Props { - hasPreviousPage: boolean; - hasNextPage: boolean; - onPageNext: () => void; - onPagePrevious: () => void; -} - -export function ToolBarPagerButtons(props: Props) { - return ( - - - props.onPagePrevious()} - isDisabled={!props.hasPreviousPage} - data-test-subj="btnPrevPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel', - { - defaultMessage: 'Previous page in table', - } - )} - /> - - - props.onPageNext()} - isDisabled={!props.hasNextPage} - data-test-subj="btnNextPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', - { - defaultMessage: 'Next page in table', - } - )} - /> - - - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss deleted file mode 100644 index 446e852f51e05..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss +++ /dev/null @@ -1,5 +0,0 @@ -.kbnDocTable__toolBarText { - line-height: $euiLineHeight; - color: #69707D; - white-space: nowrap; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx deleted file mode 100644 index a8ebfcc9bb311..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { renderWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerText } from './tool_bar_pager_text'; - -test('it renders ToolBarPagerText without crashing', () => { - const props = { - startItem: 1, - endItem: 2, - totalItems: 3, - }; - const wrapper = renderWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx deleted file mode 100644 index 5db68952b69ca..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import './tool_bar_pager_text.scss'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; - -interface Props { - startItem: number; - endItem: number; - totalItems: number; -} - -export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { - return ( - -
- -
-
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js deleted file mode 100644 index 1a3b34c45d05e..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ /dev/null @@ -1,474 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { getFakeRow } from '../../../../__fixtures__/fake_row'; -import $ from 'jquery'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../kibana_services'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../../kibana_legacy/public/angular_bootstrap'; -import { getInnerAngularModule } from '../../get_inner_angular'; -import { createBrowserHistory } from 'history'; - -const fakeRowVals = { - time: 'time_formatted', - bytes: 'bytes_formatted', - '@timestamp': '@timestamp_formatted', - request_body: 'request_body_formatted', -}; - -describe('Doc Table', () => { - const core = coreMock.createStart(); - const dataMock = dataPluginMock.createStartContract(); - let $parentScope; - let $scope; - let $elementScope; - let timeout; - let registry = []; - - // Stub out a minimal mapping of 4 fields - let mapping; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(function (index) { - return $(this).slice(index); - }); - angular.element.prototype.filter = jest.fn(function (condition) { - return $(this).filter(condition); - }); - angular.element.prototype.toggle = jest.fn(function (name) { - return $(this).toggle(name); - }); - angular.element.prototype.is = jest.fn(function (name) { - return $(this).is(name); - }); - setServices({ - uiSettings: core.uiSettings, - filterManager: dataMock.query.filterManager, - addBasePath: (path) => path, - }); - - setDocViewsRegistry({ - addDocView(view) { - registry.push(view); - }, - getDocViewsSorted() { - return registry; - }, - resetRegistry: () => { - registry = []; - }, - }); - - getInnerAngularModule( - 'app/discover', - core, - { - data: dataMock, - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach( - angular.mock.inject(function ($rootScope, Private, $timeout) { - $parentScope = $rootScope; - timeout = $timeout; - $parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - mapping = $parentScope.indexPattern.fields; - - // Stub `getConverterFor` for a field in the indexPattern to return mock data. - - const convertFn = (value, type, options) => { - const fieldName = _.get(options, 'field.name', null); - return fakeRowVals[fieldName] || ''; - }; - $parentScope.indexPattern.getFormatterForField = () => ({ - convert: convertFn, - getConverterFor: () => convertFn, - }); - }) - ); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - delete angular.element.prototype.toggle; - delete angular.element.prototype.is; - }); - - // Sets up the directive, take an element, and a list of properties to attach to the parent scope. - const init = function ($elem, props) { - angular.mock.inject(function ($compile) { - _.assign($parentScope, props); - const el = $compile($elem)($parentScope); - $elementScope = el.scope(); - el.scope().$digest(); - $scope = el.isolateScope(); - }); - }; - - const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); - }; - - // For testing column removing/adding for the header and the rows - const columnTests = function (elemType, parentElem) { - test('should create a time column if the timefield is defined', () => { - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(1); - }); - - test('should be able to add and remove columns', () => { - let childElems; - - // Should include a column for toggling and the time column by default - $parentScope.columns = ['bytes']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('bytes'); - - $parentScope.columns = ['bytes', 'request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(3); - expect($(childElems[2]).text()).toContain('request_body'); - - $parentScope.columns = ['request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('request_body'); - }); - - test('should create only the toggle column if there is no timeField', () => { - delete $scope.indexPattern.timeFieldName; - $scope.$digest(); - timeout.flush(); - - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(0); - }); - }; - - describe('kbnTableRow', () => { - const $elem = $( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row, - columns: [], - sorting: [], - filter: sinon.spy(), - maxLength: 50, - }); - }); - afterEach(() => { - destroy(); - }); - - describe('adding and removing columns', () => { - columnTests('[data-test-subj~="docTableField"]', $elem); - }); - - describe('details row', () => { - test('should be an empty tr by default', () => { - expect($elem.next().is('tr')).toBe(true); - expect($elem.next().text()).toBe(''); - }); - - test('should expand the detail row when the toggle arrow is clicked', () => { - $elem.children(':first-child').click(); - expect($elem.next().text()).not.toBe(''); - }); - - describe('expanded', () => { - let $details; - beforeEach(() => { - // Open the row - $scope.toggleRow(); - timeout.flush(); - $details = $elem.next(); - }); - afterEach(() => { - // Close the row - $scope.toggleRow(); - }); - - test('should be a tr with something in it', () => { - expect($details.is('tr')).toBe(true); - expect($details.text()).toBeTruthy(); - }); - }); - }); - }); - - describe('kbnTableRow meta', () => { - const $elem = angular.element( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row: row, - columns: [], - sorting: [], - filtering: sinon.spy(), - maxLength: 50, - }); - - // Open the row - $scope.toggleRow(); - $scope.$digest(); - timeout.flush(); - $elem.next(); - }); - - afterEach(() => { - destroy(); - }); - - /** this no longer works with the new plugin approach - test('should render even when the row source contains a field with the same name as a meta field', () => { - setTimeout(() => { - //this should be overridden by later changes - }, 100); - expect($details.find('tr').length).toBe(_.keys($parentScope.indexPattern.flattenHit($scope.row)).length); - }); */ - }); - - describe('row diffing', () => { - let $row; - let $scope; - let $root; - let $before; - - beforeEach( - angular.mock.inject(function ($rootScope, $compile, Private) { - $root = $rootScope; - $root.row = getFakeRow(0, mapping); - $root.columns = ['_source']; - $root.sorting = []; - $root.filtering = sinon.spy(); - $root.maxLength = 50; - $root.mapping = mapping; - $root.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - $row = $('').attr({ - 'kbn-table-row': 'row', - columns: 'columns', - sorting: 'sorting', - filtering: 'filtering', - 'index-pattern': 'indexPattern', - }); - - $scope = $root.$new(); - $compile($row)($scope); - $root.$apply(); - - $before = $row.find('td'); - expect($before).toHaveLength(3); - expect($before.eq(0).text().trim()).toBe(''); - expect($before.eq(1).text().trim()).toMatch(/^time_formatted/); - }) - ); - - afterEach(() => { - $row.remove(); - }); - - test('handles a new column', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - }); - - test('handles two new columns at once', () => { - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(5); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles three new columns in odd places', () => { - $root.columns = ['@timestamp', 'bytes', '_source', 'request_body']; - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after[4].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(5).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles a removed column', () => { - _.pull($root.columns, '_source'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles two removed columns', () => { - // first add a column - $root.columns.push('@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.pop(); - $root.columns.pop(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles three removed random columns', () => { - // first add two column - $root.columns.push('@timestamp', 'bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(5); - - $root.columns[0] = false; // _source - $root.columns[2] = false; // bytes - $root.columns = $root.columns.filter(Boolean); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(3); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - }); - - test('handles two columns with the same content', () => { - const tempVal = fakeRowVals.request_body; - fakeRowVals.request_body = 'bytes_formatted'; - - $root.columns.length = 0; - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after.eq(2).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - fakeRowVals.request_body = tempVal; - }); - - test('handles two columns swapping position', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[3].outerHTML); - expect($after[3].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles four columns all reversing position', () => { - $root.columns.push('bytes', 'response', '@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(6); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[5].outerHTML); - expect($after[3].outerHTML).toBe($mid[4].outerHTML); - expect($after[4].outerHTML).toBe($mid[3].outerHTML); - expect($after[5].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles multiple columns with the same name', () => { - $root.columns.push('bytes', 'bytes', 'bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(5).text().trim()).toMatch(/^bytes_formatted/); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts deleted file mode 100644 index 0f6c86df0db64..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ /dev/null @@ -1,37 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { TableHeader } from './table_header/table_header'; -import { getServices } from '../../../../kibana_services'; -import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createTableHeaderDirective(reactDirective: any) { - const { uiSettings: config } = getServices(); - - return reactDirective( - TableHeader, - [ - ['columns', { watchDepth: 'collection' }], - ['hideTimeColumn', { watchDepth: 'value' }], - ['indexPattern', { watchDepth: 'reference' }], - ['isShortDots', { watchDepth: 'value' }], - ['onChangeSortOrder', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['sortOrder', { watchDepth: 'collection' }], - ], - { restrict: 'A' }, - { - hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - isShortDots: config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), - defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), - } - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts deleted file mode 100644 index 1d6956fc80920..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ /dev/null @@ -1,230 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { find } from 'lodash'; -import $ from 'jquery'; -import openRowHtml from './table_row/open.html'; -import detailsHtml from './table_row/details.html'; -import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { getServices } from '../../../../kibana_services'; -import { getContextUrl } from '../../../helpers/get_context_url'; -import { formatRow, formatTopLevelObject } from '../../helpers'; -import { truncateByHeight } from './table_row/truncate_by_height'; -import { cell } from './table_row/cell'; - -// guesstimate at the minimum number of chars wide cells in the table should be -const MIN_LINE_LENGTH = 20; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createTableRowDirective($compile: ng.ICompileService) { - return { - restrict: 'A', - scope: { - columns: '=', - filter: '=', - indexPattern: '=', - row: '=kbnTableRow', - onAddColumn: '=?', - onRemoveColumn: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $el.after(''); - $el.empty(); - - // when we compile the details, we use this $scope - let $detailsScope: LazyScope; - - // when we compile the toggle button in the summary, we use this $scope - let $toggleScope; - - // toggle display of the rows details, a full list of the fields from each row - $scope.toggleRow = () => { - const $detailsTr = $el.next(); - - $scope.open = !$scope.open; - - /// - // add/remove $details children - /// - - $detailsTr.toggle($scope.open); - - if (!$scope.open) { - // close the child scope if it exists - $detailsScope.$destroy(); - // no need to go any further - return; - } else { - $detailsScope = $scope.$new(); - } - - // empty the details and rebuild it - $detailsTr.html(detailsHtml); - $detailsScope.row = $scope.row; - $detailsScope.hit = $scope.row; - $detailsScope.uriEncodedId = encodeURIComponent($detailsScope.hit._id); - - $compile($detailsTr)($detailsScope); - }; - - $scope.$watchMulti(['indexPattern.timeFieldName', 'row.highlight', '[]columns'], () => { - createSummaryRow($scope.row); - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - $scope.inlineFilter = function inlineFilter($event: any, type: string) { - const column = $($event.currentTarget).data().column; - const field = $scope.indexPattern.fields.getByName(column); - $scope.filter(field, $scope.flattenedRow[column], type); - }; - - $scope.getContextAppHref = () => { - return getContextUrl( - $scope.row._id, - $scope.indexPattern.id, - $scope.columns, - getServices().filterManager, - getServices().addBasePath - ); - }; - - $scope.getSingleDocHref = () => { - return getServices().addBasePath( - `/app/discover#/doc/${$scope.indexPattern.id}/${ - $scope.row._index - }?id=${encodeURIComponent($scope.row._id)}` - ); - }; - - // create a tr element that lists the value for each *column* - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function createSummaryRow(row: any) { - const indexPattern = $scope.indexPattern; - $scope.flattenedRow = indexPattern.flattenHit(row); - - // We just create a string here because its faster. - const newHtmls = [openRowHtml]; - - const mapping = indexPattern.fields.getByName; - const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - if (indexPattern.timeFieldName && !hideTimeColumn) { - newHtmls.push( - cell({ - timefield: true, - formatted: _displayField(row, indexPattern.timeFieldName), - filterable: mapping(indexPattern.timeFieldName).filterable && $scope.filter, - column: indexPattern.timeFieldName, - }) - ); - } - - if ($scope.columns.length === 0 && $scope.useNewFieldsApi) { - const formatted = formatRow(row, indexPattern); - - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted, - filterable: false, - column: '__document__', - }) - ); - } else { - $scope.columns.forEach(function (column: string) { - const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter; - if ($scope.useNewFieldsApi && !mapping(column) && !row.fields[column]) { - const innerColumns = Object.fromEntries( - Object.entries(row.fields).filter(([key]) => { - return key.indexOf(`${column}.`) === 0; - }) - ); - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted: formatTopLevelObject(row, innerColumns, indexPattern), - filterable: false, - column, - }) - ); - } else { - newHtmls.push( - cell({ - timefield: false, - sourcefield: column === '_source', - formatted: _displayField(row, column, true), - filterable: isFilterable, - column, - }) - ); - } - }); - } - - let $cells = $el.children(); - newHtmls.forEach(function (html, i) { - const $cell = $cells.eq(i); - if ($cell.data('discover:html') === html) return; - - const reuse = find($cells.slice(i + 1), (c) => { - return $.data(c, 'discover:html') === html; - }); - - const $target = reuse ? $(reuse).detach() : $(html); - $target.data('discover:html', html); - const $before = $cells.eq(i - 1); - if ($before.length) { - $before.after($target); - } else { - $el.append($target); - } - - // rebuild cells since we modified the children - $cells = $el.children(); - - if (!reuse) { - $toggleScope = $scope.$new(); - $compile($target)($toggleScope); - } - }); - - if ($scope.open) { - $detailsScope.row = row; - } - - // trim off cells that were not used rest of the cells - $cells.filter(':gt(' + (newHtmls.length - 1) + ')').remove(); - dispatchRenderComplete($el[0]); - } - - /** - * Fill an element with the value of a field - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function _displayField(row: any, fieldName: string, truncate = false) { - const indexPattern = $scope.indexPattern; - const text = indexPattern.formatField(row, fieldName); - - if (truncate && text.length > MIN_LINE_LENGTH) { - return truncateByHeight({ - body: text, - }); - } - - return text; - } - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts deleted file mode 100644 index c6d0d324b9bc2..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts +++ /dev/null @@ -1,138 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { cell } from './cell'; - -describe('cell renderer', () => { - it('renders a cell without filter buttons if it is not filterable', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a cell with filter buttons if it is filterable', () => { - expect( - cell({ - filterable: true, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: true, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a field that is neither a timefield or sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders the "formatted" contents without any manipulation', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot(` - "
 hey you can put HTML & stuff in here 
- " - `); - }); - - it('escapes the contents of "column" within the "data-column" attribute', () => { - expect( - cell({ - filterable: true, - column: '', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts deleted file mode 100644 index 8138e0f4a4fd8..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { escape } from 'lodash'; -import cellWithFilters from './cell_with_buttons.html'; -import cellWithoutFilters from './cell_without_buttons.html'; - -const TAGS_WITH_WS = />\s+<'); -} - -const cellWithFiltersTemplate = noWhiteSpace(cellWithFilters); -const cellWithoutFiltersTemplate = noWhiteSpace(cellWithoutFilters); - -interface CellProps { - timefield: boolean; - sourcefield?: boolean; - formatted: string; - filterable: boolean; - column: string; -} - -export const cell = (props: CellProps) => { - let classes = ''; - let extraAttrs = ''; - if (props.timefield) { - classes = 'eui-textNoWrap'; - extraAttrs = 'width="1%"'; - } else if (props.sourcefield) { - classes = 'eui-textBreakAll eui-textBreakWord'; - } else { - classes = 'kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord'; - } - - if (props.filterable) { - const escapedColumnContents = escape(props.column); - return cellWithFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('__column__', escapedColumnContents) - .replace('__column__', escapedColumnContents) - .replace('', props.formatted); - } - return cellWithoutFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('', props.formatted); -}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html deleted file mode 100644 index 99c65e6034013..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html deleted file mode 100644 index 8dc33cbfb8353..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html deleted file mode 100644 index faa3d51c19fee..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ /dev/null @@ -1,53 +0,0 @@ - -
-
-
-
- -
-
-

-
-
-
-
-
-
- -
-
- -
-
-
-
-
- -
- - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html deleted file mode 100644 index 1a5d974a1b081..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts deleted file mode 100644 index 70d8465589237..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts +++ /dev/null @@ -1,22 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { truncateByHeight } from './truncate_by_height'; - -describe('truncateByHeight', () => { - it('renders input without any formatting or escaping', () => { - expect( - truncateByHeight({ - body: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot( - `"
 hey you can put HTML & stuff in here 
"` - ); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx deleted file mode 100644 index 19913ed6de870..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx +++ /dev/null @@ -1,85 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useRef, useEffect } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IScope } from 'angular'; -import { getServices } from '../../../kibana_services'; -import { DocTableLegacyProps, injectAngularElement } from './create_doc_table_react'; - -type AngularEmbeddableScope = IScope & { renderProps?: DocTableEmbeddableProps }; - -export interface DocTableEmbeddableProps extends Partial { - refs: HTMLElement; -} - -function getRenderFn(domNode: Element, props: DocTableEmbeddableProps) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - throw e; - } - }; -} - -export function DiscoverDocTableEmbeddable(props: DocTableEmbeddableProps) { - return ( - - - - ); -} - -function DocTableLegacyInner(renderProps: DocTableEmbeddableProps) { - const scope = useRef(); - - useEffect(() => { - if (renderProps.refs && !scope.current) { - const fn = getRenderFn(renderProps.refs, renderProps); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope?.current) { - scope.current.renderProps = { ...renderProps }; - scope.current.$applyAsync(); - } - }, [renderProps]); - - useEffect(() => { - return () => { - scope.current?.$destroy(); - }; - }, []); - return ; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx deleted file mode 100644 index 73a67310bf4be..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ /dev/null @@ -1,173 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular, { auto, ICompileService, IScope } from 'angular'; -import { render } from 'react-dom'; -import React, { useRef, useEffect, useState, useCallback } from 'react'; -import type { estypes } from '@elastic/elasticsearch'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getServices, IndexPattern } from '../../../kibana_services'; -import { IndexPatternField } from '../../../../../data/common'; -import { SkipBottomButton } from '../../apps/main/components/skip_bottom_button'; - -export interface DocTableLegacyProps { - columns: string[]; - searchDescription?: string; - searchTitle?: string; - onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - rows: estypes.SearchHit[]; - indexPattern: IndexPattern; - minimumVisibleRows?: number; - onAddColumn?: (column: string) => void; - onBackToTop: () => void; - onSort?: (sort: string[][]) => void; - onMoveColumn?: (columns: string, newIdx: number) => void; - onRemoveColumn?: (column: string) => void; - sampleSize: number; - sort?: string[][]; - useNewFieldsApi?: boolean; -} -export interface AngularDirective { - template: string; -} -export type AngularScope = IScope & { renderProps?: DocTableLegacyProps }; - -/** - * Compiles and injects the give angular template into the given dom node - * returns a function to cleanup the injected angular element - */ -export async function injectAngularElement( - domNode: Element, - template: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderProps: any, - injector: auto.IInjectorService -) { - const rootScope: IScope = injector.get('$rootScope'); - const $compile: ICompileService = injector.get('$compile'); - const newScope = Object.assign(rootScope.$new(), { renderProps }); - - const $target = angular.element(domNode); - const $element = angular.element(template); - - newScope.$apply(() => { - const linkFn = $compile($element); - $target.empty().append($element); - linkFn(newScope); - }); - - return newScope; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getRenderFn(domNode: Element, props: any) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - render(
error
, domNode); - } - }; -} - -export function DocTableLegacy(renderProps: DocTableLegacyProps) { - const ref = useRef(null); - const scope = useRef(); - const [rows, setRows] = useState(renderProps.rows); - const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); - const onSkipBottomButtonClick = useCallback(async () => { - // delay scrolling to after the rows have been rendered - const bottomMarker = document.getElementById('discoverBottomMarker'); - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - // show all the rows - setMinimumVisibleRows(renderProps.rows.length); - - while (renderProps.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { - await wait(50); - } - bottomMarker!.focus(); - await wait(50); - bottomMarker!.blur(); - }, [setMinimumVisibleRows, renderProps.rows]); - - useEffect(() => { - setMinimumVisibleRows(50); - setRows(renderProps.rows); - }, [renderProps.rows, setMinimumVisibleRows]); - - useEffect(() => { - if (ref && ref.current && !scope.current) { - const fn = getRenderFn(ref.current, { ...renderProps, rows, minimumVisibleRows }); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope && scope.current) { - scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows }; - scope.current.$applyAsync(); - } - }, [renderProps, minimumVisibleRows, rows]); - - useEffect(() => { - return () => { - if (scope.current) { - scope.current.$destroy(); - } - }; - }, []); - return ( -
- -
- {renderProps.rows.length === renderProps.sampleSize ? ( -
- - - - -
- ) : ( - - ​ - - )} -
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html deleted file mode 100644 index ecd7aa8f3dcf4..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ /dev/null @@ -1,121 +0,0 @@ -
-
-
-
-
- {{ limitedResultsWarning }} -
- - - -
-
-
- - - - - -
-
- - -
- - - - - - -
- -
- -
-
- - -
- -

-

-
-
diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js deleted file mode 100644 index 097f32965b141..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ /dev/null @@ -1,140 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular from 'angular'; -import _ from 'lodash'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import { createBrowserHistory } from 'history'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../__fixtures__/stubbed_logstash_index_pattern'; -import hits from '../../../__fixtures__/real_hits'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; -import { setScopedHistory, setServices } from '../../../kibana_services'; -import { getInnerAngularModule } from '../get_inner_angular'; - -let $parentScope; - -let $scope; - -let $timeout; - -let indexPattern; - -const init = function ($elem, props) { - angular.mock.inject(function ($rootScope, $compile, _$timeout_) { - $timeout = _$timeout_; - $parentScope = $rootScope; - _.assign($parentScope, props); - - $compile($elem)($parentScope); - - // I think the prereq requires this? - $timeout(() => { - $elem.scope().$digest(); - }, 0); - - $scope = $elem.isolateScope(); - }); -}; - -const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); -}; - -describe('docTable', () => { - const core = coreMock.createStart(); - let $elem; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(() => { - return null; - }); - angular.element.prototype.filter = jest.fn(() => { - return { - remove: jest.fn(), - }; - }); - setServices({ - uiSettings: core.uiSettings, - }); - getInnerAngularModule( - 'app/discover', - core, - { - data: dataPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach(() => { - $elem = angular.element(` - - `); - angular.mock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }); - init($elem, { - indexPattern, - hits: [...hits], - totalHitCount: hits.length, - columns: [], - sorting: ['@timestamp', 'desc'], - }); - $scope.$digest(); - }); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - destroy(); - }); - - test('should compile', () => { - expect($elem.text()).toBeTruthy(); - }); - - test('should have an addRows function that increases the row count', () => { - expect($scope.addRows).toBeInstanceOf(Function); - $scope.$digest(); - expect($scope.limit).toBe(50); - $scope.addRows(); - expect($scope.limit).toBe(100); - }); - - test('should reset the row limit when results are received', () => { - $scope.limit = 100; - expect($scope.limit).toBe(100); - $scope.hits = [...hits]; - $scope.$digest(); - expect($scope.limit).toBe(50); - }); - - test('should have a header and a table element', () => { - $scope.$digest(); - - expect($elem.find('thead').length).toBe(1); - expect($elem.find('table').length).toBe(1); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts deleted file mode 100644 index 64c045a682296..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ /dev/null @@ -1,98 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import html from './doc_table.html'; -import { dispatchRenderComplete } from '../../../../../kibana_utils/public'; -import { SAMPLE_SIZE_SETTING } from '../../../../common'; -// @ts-expect-error -import { getLimitedSearchResultsMessage } from './doc_table_strings'; -import { getServices } from '../../../kibana_services'; -import './index.scss'; - -export interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDocTableDirective(pagerFactory: any, $filter: any) { - return { - restrict: 'E', - template: html, - scope: { - sorting: '=', - columns: '=', - hits: '=', - totalHitCount: '=', - indexPattern: '=', - isLoading: '=?', - infiniteScroll: '=?', - filter: '=?', - minimumVisibleRows: '=?', - onAddColumn: '=?', - onChangeSortOrder: '=?', - onMoveColumn: '=?', - onRemoveColumn: '=?', - inspectorAdapters: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $scope.persist = { - sorting: $scope.sorting, - columns: $scope.columns, - }; - - const limitTo = $filter('limitTo'); - const calculateItemsOnPage = () => { - $scope.pager.setTotalItems($scope.hits.length); - $scope.pageOfItems = limitTo($scope.hits, $scope.pager.pageSize, $scope.pager.startIndex); - }; - - $scope.limitedResultsWarning = getLimitedSearchResultsMessage( - getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500) - ); - - $scope.addRows = function () { - $scope.limit += 50; - }; - - $scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => { - $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50); - }); - - $scope.$watch('hits', (hits: unknown[]) => { - if (!hits) return; - - // Reset infinite scroll limit - $scope.limit = $scope.minimumVisibleRows || 50; - - if (hits.length === 0) { - dispatchRenderComplete($el[0]); - } - - if ($scope.infiniteScroll) return; - $scope.pager = pagerFactory.create(hits.length, 50, 1); - calculateItemsOnPage(); - }); - - $scope.pageOfItems = []; - $scope.onPageNext = () => { - $scope.pager.nextPage(); - calculateItemsOnPage(); - }; - - $scope.onPagePrevious = () => { - $scope.pager.previousPage(); - calculateItemsOnPage(); - }; - - $scope.shouldShowLimitedResultsWarning = () => - !$scope.pager.hasNextPage && $scope.pager.totalItems < $scope.totalHitCount; - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js b/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js deleted file mode 100644 index aac4b9cfe155d..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -/** - * A message letting the user know the results that have been retrieved is limited - * to a certain size. - * @param resultCount {Number} - */ -export function getLimitedSearchResultsMessage(resultCount) { - return i18n.translate('discover.docTable.limitedSearchResultLabel', { - defaultMessage: 'Limited to {resultCount} results. Refine your search.', - values: { resultCount }, - }); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts deleted file mode 100644 index 2029354376f26..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createInfiniteScrollDirective() { - return { - restrict: 'E', - scope: { - more: '=', - }, - link: ($scope: LazyScope, $element: JQuery) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let checkTimer: any; - /** - * depending on which version of Discover is displayed, different elements are scrolling - * and have therefore to be considered for calculation of infinite scrolling - */ - const scrollDiv = $element.parents('.dscTable'); - const scrollDivMobile = $(window); - - function onScroll() { - if (!$scope.more) return; - const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; - const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; - const scrollTop = usedScrollDiv.scrollTop(); - const scrollOffset = usedScrollDiv.prop('offsetTop') || 0; - - const winHeight = Number(usedScrollDiv.height()); - const winBottom = Number(winHeight) + Number(scrollTop); - const elTop = $element.get(0).offsetTop || 0; - const remaining = elTop - scrollOffset - winBottom; - - if (remaining <= winHeight) { - $scope[$scope.$$phase ? '$eval' : '$apply'](function () { - $scope.more(); - }); - } - } - - function scheduleCheck() { - if (checkTimer) return; - checkTimer = setTimeout(function () { - checkTimer = null; - onScroll(); - }, 50); - } - - scrollDiv.on('scroll', scheduleCheck); - window.addEventListener('scroll', scheduleCheck); - $scope.$on('$destroy', function () { - clearTimeout(checkTimer); - scrollDiv.off('scroll', scheduleCheck); - window.removeEventListener('scroll', scheduleCheck); - }); - scheduleCheck(); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js deleted file mode 100644 index db99dbe76d99f..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js +++ /dev/null @@ -1,10 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import './pager_factory'; -export { Pager } from './pager'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js deleted file mode 100644 index 1bd27a8854ca3..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js +++ /dev/null @@ -1,66 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -function clamp(val, min, max) { - return Math.min(Math.max(min, val), max); -} - -export class Pager { - constructor(totalItems, pageSize, startingPage) { - this.currentPage = startingPage; - this.totalItems = totalItems; - this.pageSize = pageSize; - this.startIndex = 0; - this.updateMeta(); - } - - get pageCount() { - return Math.ceil(this.totalItems / this.pageSize); - } - - get hasNextPage() { - return this.currentPage < this.totalPages; - } - - get hasPreviousPage() { - return this.currentPage > 1; - } - - nextPage() { - this.currentPage += 1; - this.updateMeta(); - } - - previousPage() { - this.currentPage -= 1; - this.updateMeta(); - } - - setTotalItems(count) { - this.totalItems = count; - this.updateMeta(); - } - - setPageSize(count) { - this.pageSize = count; - this.updateMeta(); - } - - updateMeta() { - this.totalPages = Math.ceil(this.totalItems / this.pageSize); - this.currentPage = clamp(this.currentPage, 1, this.totalPages); - - this.startItem = (this.currentPage - 1) * this.pageSize + 1; - this.startItem = clamp(this.startItem, 0, this.totalItems); - - this.endItem = this.startItem - 1 + this.pageSize; - this.endItem = clamp(this.endItem, 0, this.totalItems); - - this.startIndex = this.startItem - 1; - } -} diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts deleted file mode 100644 index 7cd36d419969e..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-expect-error -import { Pager } from './pager'; - -export function createPagerFactory() { - return { - create(...args: unknown[]) { - return new Pager(...args); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 992d82795302b..5f459c369ce4d 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -19,19 +19,8 @@ import { CoreStart, PluginInitializerContext } from 'kibana/public'; import { DataPublicPluginStart } from '../../../../data/public'; import { Storage } from '../../../../kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; -import { createDocTableDirective } from './doc_table'; -import { createTableHeaderDirective } from './doc_table/components/table_header'; -import { - createToolBarPagerButtonsDirective, - createToolBarPagerTextDirective, -} from './doc_table/components/pager'; import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; -import { createTableRowDirective } from './doc_table/components/table_row'; -import { createPagerFactory } from './doc_table/lib/pager/pager_factory'; -import { createInfiniteScrollDirective } from './doc_table/infinite_scroll'; -import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; -import { createRenderCompleteDirective } from './directives/render_complete'; import { configureAppAngularModule, PrivateProvider, @@ -83,7 +72,6 @@ export function initializeInnerAngularModule( createLocalPrivateModule(); createLocalPromiseModule(); createLocalStorageModule(); - createPagerFactoryModule(); createDocTableModule(); initialized = true; } @@ -97,12 +85,10 @@ export function initializeInnerAngularModule( 'discoverI18n', 'discoverPrivate', 'discoverDocTable', - 'discoverPagerFactory', 'discoverPromise', ]) .config(watchMultiDecorator) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)) - .directive('renderComplete', createRenderCompleteDirective); + .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } return angular @@ -116,11 +102,9 @@ export function initializeInnerAngularModule( 'discoverPromise', 'discoverLocalStorageProvider', 'discoverDocTable', - 'discoverPagerFactory', ]) .config(watchMultiDecorator) .run(registerListenEventListener) - .directive('renderComplete', createRenderCompleteDirective) .directive('discover', createDiscoverDirective); } @@ -153,20 +137,9 @@ const createLocalStorageService = function (type: string) { }; }; -function createPagerFactoryModule() { - angular.module('discoverPagerFactory', []).factory('pagerFactory', createPagerFactory); -} - function createDocTableModule() { angular - .module('discoverDocTable', ['discoverPagerFactory', 'react']) - .directive('docTable', createDocTableDirective) - .directive('kbnTableHeader', createTableHeaderDirective) - .directive('toolBarPagerText', createToolBarPagerTextDirective) - .directive('kbnTableRow', createTableRowDirective) - .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) - .directive('kbnInfiniteScroll', createInfiniteScrollDirective) + .module('discoverDocTable', ['react']) .directive('discoverGrid', createDiscoverGridDirective) - .directive('docViewer', createDocViewerDirective) .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 6a7f75b7e81a2..a7d9d4581d989 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index e75add7910b74..c4f6415c771f9 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -14,5 +14,4 @@ import 'angular-route'; import './discover'; import './doc'; import './context'; -import './doc_viewer'; import './redirect'; diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss similarity index 86% rename from src/plugins/discover/public/application/angular/doc_table/_doc_table.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss index ead426fa9c4eb..add2d4e753c60 100644 --- a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss @@ -2,7 +2,7 @@ * 1. Stack content vertically so the table can scroll when its constrained by a fixed container height. */ // stylelint-disable selector-no-qualifying-type -doc-table { +.kbnDocTableWrapper { @include euiScrollBar; overflow: auto; flex: 1 1 100%; @@ -21,6 +21,18 @@ doc-table { z-index: $euiZLevel1; opacity: .5; } + + // SASSTODO: add a monospace modifier to the doc-table component + .kbnDocTable__row { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeXS; + } +} + +.kbnDocTable__footer { + background-color: $euiColorLightShade; + padding: $euiSizeXS $euiSizeS; + text-align: center; } .kbnDocTable__container.loading { @@ -28,8 +40,6 @@ doc-table { } .kbnDocTable { - font-size: $euiFontSizeXS; - th { white-space: nowrap; padding-right: $euiSizeS; @@ -84,19 +94,6 @@ doc-table { } } -.kbnDocTable__bar { - margin: $euiSizeXS $euiSizeXS 0; -} - -.kbnDocTable__bar--footer { - position: relative; - margin: -($euiSize * 3) $euiSizeXS 0; -} - -.kbnDocTable__padBottom { - padding-bottom: $euiSizeXL; -} - .kbnDocTable__error { display: flex; flex-direction: column; diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts similarity index 86% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts index e1aa96f4625de..3b73044b68e07 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts @@ -7,11 +7,11 @@ */ import { getStateColumnActions } from './columns'; -import { configMock } from '../../../../__mocks__/config'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; -import { Capabilities } from '../../../../../../../core/types'; -import { AppState } from '../../../apps/main/services/discover_state'; +import { configMock } from '../../../../../../__mocks__/config'; +import { indexPatternMock } from '../../../../../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../../../../../__mocks__/index_patterns'; +import { Capabilities } from '../../../../../../../../../core/types'; +import { AppState } from '../../../services/discover_state'; function getStateColumnAction(state: {}, setAppState: (state: Partial) => void) { return getStateColumnActions({ diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts similarity index 92% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 9ef5d45947afb..130b43539d9b5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -6,17 +6,17 @@ * Side Public License, v 1. */ import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { popularizeField } from '../../../helpers/popularize_field'; -import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../../../common'; +import { popularizeField } from '../../../../../../application/helpers/popularize_field'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../../../apps/main/services/discover_state'; +} from '../../../../../../application/apps/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../../context_state'; -import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +} from '../../../../../../application/angular/context_state'; +import { IndexPattern, IndexPatternsContract } from '../../../../../../../../data/public'; /** * Helper function to provide a fallback to a single _source column if the given array of columns diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx new file mode 100644 index 0000000000000..878a9b8162628 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n/'; + +interface ToolBarPaginationProps { + pageSize: number; + pageCount: number; + activePage: number; + onPageClick: (page: number) => void; + onPageSizeChange: (size: number) => void; +} + +export const ToolBarPagination = ({ + pageSize, + pageCount, + activePage, + onPageSizeChange, + onPageClick, +}: ToolBarPaginationProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const rowsWord = i18n.translate('discover.docTable.rows', { + defaultMessage: 'rows', + }); + + const onChooseRowsClick = () => setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + + const closePopover = () => setIsPopoverOpen(false); + + const getIconType = (size: number) => { + return size === pageSize ? 'check' : 'empty'; + }; + + const rowsPerPageOptions = [25, 50, 100].map((cur) => ( + { + closePopover(); + onPageSizeChange(cur); + }} + > + {cur} {rowsWord} + + )); + + return ( + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index a75aea7169737..5afa26d35b4f5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx similarity index 99% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx index 48ea7ffc46384..7b72e94169cfe 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { TableHeader } from './table_header'; import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; -import { IndexPattern, IndexPatternField } from '../../../../../kibana_services'; +import { IndexPattern, IndexPatternField } from '../../../../../../../kibana_services'; function getMockIndexPattern() { return ({ diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index 57f7382bd98ca..cb8198f1d6d6a 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx new file mode 100644 index 0000000000000..59ced9d5668a0 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl, findTestSubject } from '@kbn/test/jest'; +import { TableRow, TableRowProps } from './table_row'; +import { setDocViewsRegistry, setServices } from '../../../../../../kibana_services'; +import { createFilterManagerMock } from '../../../../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { DiscoverServices } from '../../../../../../build_services'; +import { indexPatternWithTimefieldMock } from '../../../../../../__mocks__/index_pattern_with_timefield'; +import { uiSettingsMock } from '../../../../../../__mocks__/ui_settings'; +import { DocViewsRegistry } from '../../../../../doc_views/doc_views_registry'; + +jest.mock('../lib/row_formatter', () => { + const originalModule = jest.requireActual('../lib/row_formatter'); + return { + ...originalModule, + formatRow: () => mocked_document_cell, + }; +}); + +const mountComponent = (props: TableRowProps) => { + return mountWithIntl( + + + + +
+ ); +}; + +const mockHit = { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, +}; + +const mockFilterManager = createFilterManagerMock(); + +describe('Doc table row component', () => { + let mockInlineFilter; + let defaultProps: TableRowProps; + + beforeEach(() => { + mockInlineFilter = jest.fn(); + + defaultProps = ({ + columns: ['_source'], + filter: mockInlineFilter, + indexPattern: indexPatternWithTimefieldMock, + row: mockHit, + useNewFieldsApi: true, + filterManager: mockFilterManager, + addBasePath: (path: string) => path, + hideTimeColumn: true, + } as unknown) as TableRowProps; + + setServices(({ + uiSettings: uiSettingsMock, + } as unknown) as DiscoverServices); + + setDocViewsRegistry(new DocViewsRegistry()); + }); + + it('should render __document__ column', () => { + const component = mountComponent({ ...defaultProps, columns: [] }); + const docTableField = findTestSubject(component, 'docTableField'); + expect(docTableField.first().text()).toBe('mocked_document_cell'); + }); + + it('should render message, _index and bytes fields', () => { + const component = mountComponent({ ...defaultProps, columns: ['message', '_index', 'bytes'] }); + + const fields = findTestSubject(component, 'docTableField'); + expect(fields.first().text()).toBe('mock_message'); + expect(fields.last().text()).toBe('20'); + expect(fields.length).toBe(3); + }); + + describe('details row', () => { + it('should be empty by default', () => { + const component = mountComponent(defaultProps); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + }); + + it('should expand the detail row when the toggle arrow is clicked', () => { + const component = mountComponent(defaultProps); + const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn'); + + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + toggleButton.simulate('click'); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeTruthy(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx new file mode 100644 index 0000000000000..886aeffc06667 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment, useCallback, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; +import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; +import { TableCell } from './table_row/table_cell'; +import { ElasticSearchHit, DocViewFilterFn } from '../../../../../doc_views/doc_views_types'; +import { trimAngularSpan } from '../../../../../components/table/table_helper'; +import { getContextUrl } from '../../../../../helpers/get_context_url'; +import { getSingleDocUrl } from '../../../../../helpers/get_single_doc_url'; +import { TableRowDetails } from './table_row_details'; +import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; + +export type DocTableRow = ElasticSearchHit & { + isAnchor?: boolean; +}; + +export interface TableRowProps { + columns: string[]; + filter: DocViewFilterFn; + indexPattern: IndexPattern; + row: DocTableRow; + onAddColumn?: (column: string) => void; + onRemoveColumn?: (column: string) => void; + useNewFieldsApi: boolean; + hideTimeColumn: boolean; + filterManager: FilterManager; + addBasePath: (path: string) => string; +} + +export const TableRow = ({ + columns, + filter, + row, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, +}: TableRowProps) => { + const [open, setOpen] = useState(false); + const docTableRowClassName = classNames('kbnDocTable__row', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'kbnDocTable__row--highlight': row.isAnchor, + }); + const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : ''; + + const flattenedRow = useMemo(() => indexPattern.flattenHit(row), [indexPattern, row]); + const mapping = useMemo(() => indexPattern.fields.getByName, [indexPattern]); + + // toggle display of the rows details, a full list of the fields from each row + const toggleRow = () => setOpen((prevOpen) => !prevOpen); + + /** + * Fill an element with the value of a field + */ + const displayField = (fieldName: string) => { + const text = indexPattern.formatField(row, fieldName); + const formattedField = trimAngularSpan(String(text)); + + // field formatters take care of escaping + // eslint-disable-next-line react/no-danger + const fieldElement = ; + + return
{fieldElement}
; + }; + const inlineFilter = useCallback( + (column: string, type: '+' | '-') => { + const field = indexPattern.fields.getByName(column); + filter(field!, flattenedRow[column], type); + }, + [filter, flattenedRow, indexPattern.fields] + ); + + const getContextAppHref = () => { + return getContextUrl(row._id, indexPattern.id!, columns, filterManager, addBasePath); + }; + + const getSingleDocHref = () => { + return addBasePath(getSingleDocUrl(indexPattern.id!, row._index, row._id)); + }; + + const rowCells = [ + + + {open ? ( + + ) : ( + + )} + + , + ]; + + if (indexPattern.timeFieldName && !hideTimeColumn) { + rowCells.push( + + ); + } + + if (columns.length === 0 && useNewFieldsApi) { + const formatted = formatRow(row, indexPattern); + + rowCells.push( + + ); + } else { + columns.forEach(function (column: string) { + // when useNewFieldsApi is true, addressing to the fields property is safe + if (useNewFieldsApi && !mapping(column) && !row.fields![column]) { + const innerColumns = Object.fromEntries( + Object.entries(row.fields!).filter(([key]) => { + return key.indexOf(`${column}.`) === 0; + }) + ); + + rowCells.push( + + ); + } else { + const isFilterable = Boolean(mapping(column)?.filterable && filter); + rowCells.push( + + ); + } + }); + } + + return ( + + + {rowCells} + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap new file mode 100644 index 0000000000000..5f3564174adf8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Doc table cell component renders a cell with filter buttons if it is filterable 1`] = ` + + formatted content +
+ } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + + + + + + + + + + + + + + +`; + +exports[`Doc table cell component renders a cell without filter buttons if it is not filterable 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a field that is neither a timefield or sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={false} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={true} + timefield={false} +> + + + formatted content + + + + +`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss similarity index 64% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss index e175a2f3383e2..2e643c195208c 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss @@ -3,7 +3,15 @@ } .kbnDocTableCell__toggleDetails { - padding: $euiSizeXS 0 0 0!important; + padding: $euiSizeXS 0 0 0 !important; +} + +/** + * Fixes time column width in Firefox after toggle display of the rows details. + * Described issue - https://github.com/elastic/kibana/pull/104361#issuecomment-894271241 + */ +.kbnDocTableCell--extraWidth { + width: 1%; } .kbnDocTableCell__filter { @@ -12,6 +20,11 @@ right: 0; } +.kbnDocTableCell__filterButton { + font-size: $euiFontSizeXS; + padding: $euiSizeXS; +} + /** * 1. Align icon with text in cell. * 2. Use opacity to make this element accessible to screen readers and keyboard. diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx new file mode 100644 index 0000000000000..316c2b27357a9 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { CellProps, TableCell } from './table_cell'; + +const mountComponent = (props: Omit) => { + return mount( {}} />); +}; + +describe('Doc table cell component', () => { + test('renders a cell without filter buttons if it is not filterable', () => { + const component = mountComponent({ + filterable: false, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }); + expect(component).toMatchSnapshot(); + }); + + it('renders a cell with filter buttons if it is filterable', () => { + expect( + mountComponent({ + filterable: true, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: true, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a field that is neither a timefield or sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx new file mode 100644 index 0000000000000..ad2368439d6d8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import classNames from 'classnames'; +import { TableCellActions } from './table_cell_actions'; +export interface CellProps { + timefield: boolean; + sourcefield?: boolean; + formatted: JSX.Element; + filterable: boolean; + column: string; + inlineFilter: (column: string, type: '+' | '-') => void; +} + +export const TableCell = (props: CellProps) => { + const classes = classNames({ + ['eui-textNoWrap kbnDocTableCell--extraWidth']: props.timefield, + ['eui-textBreakAll eui-textBreakWord']: props.sourcefield, + ['kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord']: + !props.timefield && !props.sourcefield, + }); + + const handleFilterFor = () => props.inlineFilter(props.column, '+'); + const handleFilterOut = () => props.inlineFilter(props.column, '-'); + + return ( + + {props.formatted} + {props.filterable ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx new file mode 100644 index 0000000000000..f252c8d801399 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface TableCellActionsProps { + handleFilterFor: () => void; + handleFilterOut: () => void; +} + +export const TableCellActions = ({ handleFilterFor, handleFilterOut }: TableCellActionsProps) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx new file mode 100644 index 0000000000000..c3ff53fe2d3a8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface TableRowDetailsProps { + open: boolean; + colLength: number; + isTimeBased: boolean; + getContextAppHref: () => string; + getSingleDocHref: () => string; + children: JSX.Element; +} + +export const TableRowDetails = ({ + open, + colLength, + isTimeBased, + getContextAppHref, + getSingleDocHref, + children, +}: TableRowDetailsProps) => { + if (!open) { + return null; + } + + return ( + + + + + + + + + +

+ +

+
+
+
+
+ + + + {isTimeBased && ( + + + + )} + + + + + + + + +
+
{children}
+ + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx new file mode 100644 index 0000000000000..c745fbf64d294 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { DocTableEmbeddable, DocTableEmbeddableProps } from './doc_table_embeddable'; + +export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) { + return ( + + + + ); +} diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx new file mode 100644 index 0000000000000..8d29efec73716 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment } from 'react'; +import './index.scss'; +import { SkipBottomButton } from '../skip_bottom_button'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; + +const DocTableWrapperMemoized = React.memo(DocTableWrapper); + +const renderDocTable = (tableProps: DocTableRenderProps) => { + return ( + + + + {tableProps.renderHeader()} + {tableProps.renderRows(tableProps.rows)} +
+ + ​ + +
+ ); +}; + +export const DocTableContext = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx new file mode 100644 index 0000000000000..04902af692b74 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { SAMPLE_SIZE_SETTING } from '../../../../../../common'; +import { usePager } from './lib/use_pager'; +import { ToolBarPagination } from './components/pager/tool_bar_pagination'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { TotalDocuments } from '../total_documents/total_documents'; +import { getServices } from '../../../../../kibana_services'; + +export interface DocTableEmbeddableProps extends DocTableProps { + totalHitCount: number; +} + +const DocTableWrapperMemoized = memo(DocTableWrapper); + +export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { + const pager = usePager({ totalItems: props.rows.length }); + + const pageOfItems = useMemo( + () => props.rows.slice(pager.startIndex, pager.pageSize + pager.startIndex), + [pager.pageSize, pager.startIndex, props.rows] + ); + + const shouldShowLimitedResultsWarning = () => + !pager.hasNextPage && props.rows.length < props.totalHitCount; + + const scrollTop = () => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + }; + + const onPageChange = (page: number) => { + scrollTop(); + pager.onPageChange(page); + }; + + const onPageSizeChange = (size: number) => { + scrollTop(); + pager.onPageSizeChange(size); + }; + + const sampleSize = useMemo(() => { + return getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500); + }, []); + + const renderDocTable = useCallback( + (renderProps: DocTableRenderProps) => { + return ( +
+ + {renderProps.renderHeader()} + {renderProps.renderRows(pageOfItems)} +
+
+ ); + }, + [pageOfItems] + ); + + return ( + + + + {shouldShowLimitedResultsWarning() && ( + + + + + + )} + {props.totalHitCount !== 0 && ( + + + + )} + + + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx new file mode 100644 index 0000000000000..8e9066151b368 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment, memo, useCallback, useEffect, useState } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { debounce } from 'lodash'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { SkipBottomButton } from '../skip_bottom_button'; + +const DocTableInfiniteContent = (props: DocTableRenderProps) => { + const [limit, setLimit] = useState(props.minimumVisibleRows); + + // Reset infinite scroll limit + useEffect(() => { + setLimit(props.minimumVisibleRows); + }, [props.rows, props.minimumVisibleRows]); + + /** + * depending on which version of Discover is displayed, different elements are scrolling + * and have therefore to be considered for calculation of infinite scrolling + */ + useEffect(() => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + const scrollMobileElem = document.documentElement; + + const scheduleCheck = debounce(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv; + + const scrollusedHeight = usedScrollDiv.scrollHeight; + const scrollTop = Math.abs(usedScrollDiv.scrollTop); + const clientHeight = usedScrollDiv.clientHeight; + + if (scrollTop + clientHeight === scrollusedHeight) { + setLimit((prevLimit) => prevLimit + 50); + } + }, 50); + + scrollDiv.addEventListener('scroll', scheduleCheck); + window.addEventListener('scroll', scheduleCheck); + + scheduleCheck(); + + return () => { + scrollDiv.removeEventListener('scroll', scheduleCheck); + window.removeEventListener('scroll', scheduleCheck); + }; + }, []); + + const onBackToTop = useCallback(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const focusElem = document.querySelector('.dscTable') as HTMLElement; + focusElem.focus(); + + // Only the desktop one needs to target a specific container + if (!isMobileView) { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, []); + + return ( + + + + {props.renderHeader()} + {props.renderRows(props.rows.slice(0, limit))} +
+ {props.rows.length === props.sampleSize ? ( +
+ + + + +
+ ) : ( + + ​ + + )} +
+ ); +}; + +const DocTableWrapperMemoized = memo(DocTableWrapper); +const DocTableInfiniteContentMemoized = memo(DocTableInfiniteContent); + +const renderDocTable = (tableProps: DocTableRenderProps) => ( + +); + +export const DocTableInfinite = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx new file mode 100644 index 0000000000000..df5869bd61e52 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { findTestSubject, mountWithIntl } from '@kbn/test/jest'; +import { setServices } from '../../../../../kibana_services'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { DocTableWrapper, DocTableWrapperProps } from './doc_table_wrapper'; +import { DocTableRow } from './components/table_row'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; + +const mountComponent = (props: DocTableWrapperProps) => { + return mountWithIntl(); +}; + +describe('Doc table component', () => { + let defaultProps: DocTableWrapperProps; + + const initDefaults = (rows?: DocTableRow[]) => { + defaultProps = { + columns: ['_source'], + indexPattern: indexPatternMock, + rows: rows || [ + { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, + }, + ], + sort: [['order_date', 'desc']], + isLoading: false, + searchDescription: '', + onAddColumn: () => {}, + onFilter: () => {}, + onMoveColumn: () => {}, + onRemoveColumn: () => {}, + onSort: () => {}, + useNewFieldsApi: true, + dataTestSubj: 'discoverDocTable', + render: () => { + return
mock
; + }, + }; + + setServices(discoverServiceMock); + }; + + it('should render infinite table correctly', () => { + initDefaults(); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeTruthy(); + expect(component.find('.kbnDocTable__error').exists()).toBeFalsy(); + }); + + it('should render error fallback if rows array is empty', () => { + initDefaults([]); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeFalsy(); + expect(component.find('.kbnDocTable__error').exists()).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx new file mode 100644 index 0000000000000..c875bf155bd79 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TableHeader } from './components/table_header/table_header'; +import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SAMPLE_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../kibana_services'; +import { SortOrder } from './components/table_header/helpers'; +import { DocTableRow, TableRow } from './components/table_row'; +import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; + +export interface DocTableProps { + /** + * Rows of classic table + */ + rows: DocTableRow[]; + /** + * Columns of classic table + */ + columns: string[]; + /** + * Current IndexPattern + */ + indexPattern: IndexPattern; + /** + * Current sorting + */ + sort: string[][]; + /** + * New fields api switch + */ + useNewFieldsApi: boolean; + /** + * Current search description + */ + searchDescription?: string; + /** + * Current shared item title + */ + sharedItemTitle?: string; + /** + * Current data test subject + */ + dataTestSubj: string; + /** + * Loading state + */ + isLoading: boolean; + /** + * Filter callback + */ + onFilter: DocViewFilterFn; + /** + * Sorting callback + */ + onSort?: (sort: string[][]) => void; + /** + * Add columns callback + */ + onAddColumn?: (column: string) => void; + /** + * Reordering column callback + */ + onMoveColumn?: (columns: string, newIdx: number) => void; + /** + * Remove column callback + */ + onRemoveColumn?: (column: string) => void; +} + +export interface DocTableRenderProps { + rows: DocTableRow[]; + minimumVisibleRows: number; + sampleSize: number; + renderRows: (row: DocTableRow[]) => JSX.Element[]; + renderHeader: () => JSX.Element; + onSkipBottomButtonClick: () => void; +} + +export interface DocTableWrapperProps extends DocTableProps { + /** + * Renders Doc table content + */ + render: (params: DocTableRenderProps) => JSX.Element; +} + +export const DocTableWrapper = ({ + render, + columns, + rows, + indexPattern, + onSort, + onAddColumn, + onMoveColumn, + onRemoveColumn, + sort, + onFilter, + useNewFieldsApi, + searchDescription, + sharedItemTitle, + dataTestSubj, + isLoading, +}: DocTableWrapperProps) => { + const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); + const [ + defaultSortOrder, + hideTimeColumn, + isShortDots, + sampleSize, + filterManager, + addBasePath, + ] = useMemo(() => { + const services = getServices(); + return [ + services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), + services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + services.uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), + services.uiSettings.get(SAMPLE_SIZE_SETTING, 500), + services.filterManager, + services.addBasePath, + ]; + }, []); + + const onSkipBottomButtonClick = useCallback(async () => { + // delay scrolling to after the rows have been rendered + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + // show all the rows + setMinimumVisibleRows(rows.length); + + while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker!.focus(); + await wait(50); + bottomMarker!.blur(); + }, [setMinimumVisibleRows, rows]); + + const renderHeader = useCallback( + () => ( + + ), + [ + columns, + defaultSortOrder, + hideTimeColumn, + indexPattern, + isShortDots, + onMoveColumn, + onRemoveColumn, + onSort, + sort, + ] + ); + + const renderRows = useCallback( + (rowsToRender: DocTableRow[]) => { + return rowsToRender.map((current) => ( + + )); + }, + [ + columns, + onFilter, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, + ] + ); + + return ( +
+ {rows.length !== 0 && + render({ + rows, + minimumVisibleRows, + sampleSize, + onSkipBottomButtonClick, + renderHeader, + renderRows, + })} + {!rows.length && ( +
+ + + + + +
+ )} +
+ ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts similarity index 90% rename from src/plugins/discover/public/application/angular/doc_table/index.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.ts index 3a8f170f8680d..513183cc99468 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { createDocTableDirective } from './doc_table'; export { getSort, getSortArray } from './lib/get_sort'; export { getSortForSearchSource } from './lib/get_sort_for_search_source'; export { getDefaultSort } from './lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts similarity index 91% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts index f181d583f0211..b2c7499b4a040 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts @@ -8,8 +8,8 @@ import { getDefaultSort } from './get_default_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('getDefaultSort function', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts similarity index 93% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts index aa1cf4a61066d..e01ff0b00e2b0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../kibana_services'; +import { IndexPattern } from '../../../../../../kibana_services'; import { isSortable } from './get_sort'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts index 19d629e14da66..865ef1d3fb729 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts @@ -8,8 +8,8 @@ import { getSort, getSortArray } from './get_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('docTable', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts index 4b16c1aa3dcc6..2c687a59ea291 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { IndexPattern } from '../../../../../../data/public'; +import { IndexPattern } from '../../../../../../../../data/public'; export type SortPairObj = Record; export type SortPairArr = [string, string]; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts similarity index 94% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts index dc7817d95dd38..3753597ced163 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts @@ -8,8 +8,8 @@ import { getSortForSearchSource } from './get_sort_for_search_source'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; describe('getSortForSearchSource function', function () { diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts similarity index 95% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts index 58a690b70529e..2bc8a71301df9 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, IndexPattern } from '../../../../kibana_services'; +import { EsQuerySortValue, IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; import { getSort } from './get_sort'; diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts similarity index 53% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts index 6b356446850e6..8c108e7d4dcf6 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ +import ReactDOM from 'react-dom/server'; import { formatRow, formatTopLevelObject } from './row_formatter'; -import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; -import { fieldFormatsMock } from '../../../../../field_formats/common/mocks'; -import { setServices } from '../../../kibana_services'; -import { DiscoverServices } from '../../../build_services'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../__mocks__/stubbed_saved_object_index_pattern'; +import { IndexPattern } from '../../../../../../../../data/common/index_patterns/index_patterns'; +import { fieldFormatsMock } from '../../../../../../../../field_formats/common/mocks'; +import { setServices } from '../../../../../../kibana_services'; +import { DiscoverServices } from '../../../../../../build_services'; describe('Row formatter', () => { const hit = { @@ -68,9 +69,42 @@ describe('Row formatter', () => { }); it('formats document properly', () => { - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
number:
42
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('limits number of rendered items', () => { @@ -79,17 +113,57 @@ describe('Row formatter', () => { get: () => 1, }, } as unknown) as DiscoverServices); - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats document with highlighted fields first', () => { - expect( - formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() - ).toMatchInlineSnapshot( - `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow({ ...hit, highlight: { number: '42' } }, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats top level objects using formatter', () => { @@ -111,10 +185,19 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects in alphabetical order', () => { @@ -124,11 +207,13 @@ describe('Row formatter', () => { indexPattern.getFormatterForField = jest.fn().mockReturnValue({ convert: () => 'formatted', }); - const formatted = formatTopLevelObject( - { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, - { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, - indexPattern - ).trim(); + const formatted = ReactDOM.renderToStaticMarkup( + formatTopLevelObject( + { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, + { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, + indexPattern + ) + ); expect(formatted.indexOf('
a.ccc:
')).toBeLessThan(formatted.indexOf('
a.zzz:
')); }); @@ -156,10 +241,23 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.keys:
formatted, formatted
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects, converting unknown fields to string', () => { @@ -177,9 +275,18 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
5, 10
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); }); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx similarity index 87% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index c410273cc7510..51e83f78f9f1c 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -7,9 +7,8 @@ */ import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom/server'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; -import { getServices, IndexPattern } from '../../../kibana_services'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../../kibana_services'; interface Props { defPairs: Array<[string, unknown]>; @@ -44,9 +43,7 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) pairs.push([displayKey ? displayKey : key, val]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; export const formatTopLevelObject = ( @@ -80,7 +77,5 @@ export const formatTopLevelObject = ( pairs.push([displayKey ? displayKey : key, formatted]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts new file mode 100644 index 0000000000000..5522e3c150213 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect, useState } from 'react'; + +interface MetaParams { + currentPage: number; + totalItems: number; + totalPages: number; + startIndex: number; + hasNextPage: boolean; + pageSize: number; +} + +interface ProvidedMeta { + updatedPageSize?: number; + updatedCurrentPage?: number; +} + +const INITIAL_PAGE_SIZE = 50; + +export const usePager = ({ totalItems }: { totalItems: number }) => { + const [meta, setMeta] = useState({ + currentPage: 0, + totalItems, + startIndex: 0, + totalPages: Math.ceil(totalItems / INITIAL_PAGE_SIZE), + hasNextPage: true, + pageSize: INITIAL_PAGE_SIZE, + }); + + const getNewMeta = useCallback( + (newMeta: ProvidedMeta) => { + const actualCurrentPage = newMeta.updatedCurrentPage ?? meta.currentPage; + const actualPageSize = newMeta.updatedPageSize ?? meta.pageSize; + + const newTotalPages = Math.ceil(totalItems / actualPageSize); + const newStartIndex = actualPageSize * actualCurrentPage; + + return { + currentPage: actualCurrentPage, + totalPages: newTotalPages, + startIndex: newStartIndex, + totalItems, + hasNextPage: meta.currentPage + 1 < meta.totalPages, + pageSize: actualPageSize, + }; + }, + [meta.currentPage, meta.pageSize, meta.totalPages, totalItems] + ); + + const onPageChange = useCallback( + (pageIndex: number) => setMeta(getNewMeta({ updatedCurrentPage: pageIndex })), + [getNewMeta] + ); + + const onPageSizeChange = useCallback( + (newPageSize: number) => + setMeta(getNewMeta({ updatedPageSize: newPageSize, updatedCurrentPage: 0 })), + [getNewMeta] + ); + + /** + * Update meta on totalItems change + */ + useEffect(() => setMeta(getNewMeta({})), [getNewMeta, totalItems]); + + return { + ...meta, + onPageChange, + onPageSizeChange, + }; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx index 1136b693c9e74..e5212e877e8ba 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx @@ -36,7 +36,6 @@ function getProps(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { return { expandedDoc: undefined, indexPattern: indexPatternMock, - isMobile: jest.fn(() => false), onAddFilter: jest.fn(), savedSearch: savedSearchMock, documents$, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx index 13cf021ff2573..e0e0c9c6f8831 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx @@ -5,11 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useRef, useMemo, useCallback, memo } from 'react'; -import { EuiFlexItem, EuiSpacer, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useMemo, useCallback, memo } from 'react'; +import { + EuiFlexItem, + EuiSpacer, + EuiText, + EuiLoadingSpinner, + EuiScreenReaderOnly, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react'; -import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; import { FetchStatus } from '../../../../types'; @@ -26,15 +30,16 @@ import { DataDocumentsMsg, DataDocuments$ } from '../../services/use_saved_searc import { DiscoverServices } from '../../../../../build_services'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { useDataState } from '../../utils/use_data_state'; +import { DocTableInfinite } from '../doc_table/doc_table_infinite'; +import { SortPairArr } from '../doc_table/lib/get_sort'; -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); function DiscoverDocumentsComponent({ documents$, expandedDoc, indexPattern, - isMobile, onAddFilter, savedSearch, services, @@ -45,7 +50,6 @@ function DiscoverDocumentsComponent({ documents$: DataDocuments$; expandedDoc?: ElasticSearchHit; indexPattern: IndexPattern; - isMobile: () => boolean; navigateTo: (url: string) => void; onAddFilter: DocViewFilterFn; savedSearch: SavedSearch; @@ -57,11 +61,11 @@ function DiscoverDocumentsComponent({ const { capabilities, indexPatterns, uiSettings } = services; const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); - const scrollableDesktop = useRef(null); const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); const documentState: DataDocumentsMsg = useDataState(documents$); + const isLoading = documentState.fetchStatus === FetchStatus.LOADING; const rows = useMemo(() => documentState.result || [], [documentState.result]); @@ -75,21 +79,6 @@ function DiscoverDocumentsComponent({ useNewFieldsApi, }); - /** - * Legacy function, remove once legacy grid is removed - */ - const onBackToTop = useCallback(() => { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }, [scrollableDesktop, isMobile]); - const onResize = useCallback( (colSettings: { columnId: string; width: number }) => { const grid = { ...state.grid } || {}; @@ -131,62 +120,57 @@ function DiscoverDocumentsComponent({ } return ( - -
-

+ + +

- {isLegacy && rows && rows.length && ( - + {isLegacy && rows && rows.length && ( + + )} + {!isLegacy && ( +
+ - )} - {!isLegacy && ( -
- -
- )} -

+
+ )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss index 485bdc65c6cb6..2401325dd76f2 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss @@ -84,17 +84,8 @@ discover-app { } .dscTable { - // SASSTODO: add a monospace modifier to the doc-table component - .kbnDocTable__row { - font-family: $euiCodeFontFamily; - font-size: $euiFontSizeXS; - } -} - -.dscTable__footer { - background-color: $euiColorLightShade; - padding: $euiSizeXS $euiSizeS; - text-align: center; + // needs for scroll container of lagacy table + min-height: 0; } .dscDocuments__loading { diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 8930722e813ce..94e28c3f1d54c 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover_layout.scss'; -import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { EuiSpacer, EuiButtonIcon, @@ -66,13 +66,11 @@ export function DiscoverLayout({ stateContainer, }: DiscoverLayoutProps) { const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services; + const { main$, charts$, totalHits$ } = savedSearchData$; const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); - const collapseIcon = useRef(null); const fetchCounter = useRef(0); - const { main$, charts$, totalHits$ } = savedSearchData$; - const dataState: DataMainMsg = useDataState(main$); useEffect(() => { @@ -81,8 +79,6 @@ export function DiscoverLayout({ } }, [dataState.fetchStatus]); - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - const isMobile = useCallback(() => collapseIcon && !collapseIcon.current, []); const timeField = useMemo(() => { return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; }, [indexPattern]); @@ -208,7 +204,6 @@ export function DiscoverLayout({ aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { defaultMessage: 'Toggle sidebar', })} - buttonRef={collapseIcon} /> @@ -261,11 +256,11 @@ export function DiscoverLayout({ /> + { + return ( + + {totalHitCount}, + }} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index 1bf9710def03c..a5a064a8fc1c6 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -24,7 +24,7 @@ import { import { useSearchSession } from './use_search_session'; import { FetchStatus } from '../../../types'; import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; +import { SortPairArr } from '../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 00473956c57e3..225d90c61de12 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -10,8 +10,8 @@ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; import { ISearchSource } from '../../../../../../data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; +import { getSortForSearchSource } from '../components/doc_table'; import { AppState } from '../services/discover_state'; -import { getSortForSearchSource } from '../../../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 426272fa8ce1c..fc835d4d3dd16 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -9,12 +9,11 @@ import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import { getSortArray } from '../../../angular/doc_table'; -import { getDefaultSort } from '../../../angular/doc_table/lib/get_default_sort'; import { SavedSearch } from '../../../../saved_searches'; import { DataPublicPluginStart } from '../../../../../../data/public'; import { AppState } from '../services/discover_state'; +import { getDefaultSort, getSortArray } from '../components/doc_table'; function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts index f7154b26c7ed6..00f194662e410 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { getSortArray } from '../../../angular/doc_table'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; import { IndexPattern } from '../../../../kibana_services'; +import { getSortArray, SortPairArr } from '../components/doc_table/lib/get_sort'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 3fac75a198d53..b4a1dab41a096 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { getSortForSearchSource } from '../../../angular/doc_table'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; import { SortOrder } from '../../../../saved_searches/types'; import { DiscoverServices } from '../../../../build_services'; import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; +import { getSortForSearchSource } from '../components/doc_table'; /** * Helper function to update the given searchSource before fetching/sharing/persisting diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index c52f22c60bb5b..37963eb2dfa93 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -25,6 +25,7 @@ import { useContextAppFetch } from './use_context_app_fetch'; import { popularizeField } from '../../helpers/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from '../../angular/context/api/context'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -161,7 +162,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} setAppState={setAppState} - addFilter={addFilter} + addFilter={addFilter as DocViewFilterFn} rows={rows} predecessors={fetchedState.predecessors} successors={fetchedState.successors} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx index 0536ab7e6a025..1b95af8bdbe1c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx @@ -8,73 +8,76 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; -import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { AppState, GetStateReturn } from '../../angular/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { EsHitRecordList } from '../../angular/context/api/context'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; -import { getServices } from '../../../kibana_services'; +import { getServices, setServices } from '../../../kibana_services'; import { LoadingStatus } from '../../angular/context_query_state'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { DiscoverGrid } from '../discover_grid/discover_grid'; - -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - uiSettings: mockUiSettings, - }), - }; -}); +import { discoverServiceMock } from '../../../__mocks__/services'; +import { DocTableWrapper } from '../../apps/main/components/doc_table/doc_table_wrapper'; describe('ContextAppContent test', () => { - const hit = { - _id: '123', - _index: 'test_index', - _score: null, - _version: 1, - _source: { - category: ["Men's Clothing"], - currency: 'EUR', - customer_first_name: 'Walker', - customer_full_name: 'Walker Texas Ranger', - customer_gender: 'MALE', - customer_last_name: 'Ranger', - }, - fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], - sort: [1603114502000, 2092], - }; - const defaultProps = ({ - columns: ['Time (@timestamp)', '_source'], - indexPattern: indexPatternMock, - appState: ({} as unknown) as AppState, - stateContainer: ({} as unknown) as GetStateReturn, - anchorStatus: LoadingStatus.LOADED, - predecessorsStatus: LoadingStatus.LOADED, - successorsStatus: LoadingStatus.LOADED, - rows: ([hit] as unknown) as EsHitRecordList, - predecessors: [], - successors: [], - defaultStepSize: 5, - predecessorCount: 10, - successorCount: 10, - useNewFieldsApi: false, - isPaginationEnabled: false, - onAddColumn: () => {}, - onRemoveColumn: () => {}, - onSetColumns: () => {}, - services: getServices(), - sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, - isLegacy: true, - setAppState: () => {}, - addFilter: () => {}, - } as unknown) as ContextAppContentProps; + let hit; + let defaultProps: ContextAppContentProps; + + beforeEach(() => { + setServices(discoverServiceMock); + + hit = { + _id: '123', + _index: 'test_index', + _score: null, + _version: 1, + fields: [ + { + order_date: ['2020-10-19T13:35:02.000Z'], + }, + ], + _source: { + category: ["Men's Clothing"], + currency: 'EUR', + customer_first_name: 'Walker', + customer_full_name: 'Walker Texas Ranger', + customer_gender: 'MALE', + customer_last_name: 'Ranger', + }, + sort: [1603114502000, 2092], + }; + defaultProps = ({ + columns: ['order_date', '_source'], + indexPattern: indexPatternMock, + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorStatus: LoadingStatus.LOADED, + predecessorsStatus: LoadingStatus.LOADED, + successorsStatus: LoadingStatus.LOADED, + rows: ([hit] as unknown) as EsHitRecordList, + predecessors: [], + successors: [], + defaultStepSize: 5, + predecessorCount: 10, + successorCount: 10, + useNewFieldsApi: true, + isPaginationEnabled: false, + onAddColumn: () => {}, + onRemoveColumn: () => {}, + onSetColumns: () => {}, + services: getServices(), + sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, + isLegacy: true, + setAppState: () => {}, + addFilter: () => {}, + } as unknown) as ContextAppContentProps; + }); it('should render legacy table correctly', () => { const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(1); + expect(component.find(DocTableWrapper).length).toBe(1); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); expect(component.find(ActionBar).length).toBe(2); @@ -84,18 +87,11 @@ describe('ContextAppContent test', () => { const props = { ...defaultProps }; props.anchorStatus = LoadingStatus.LOADING; const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(component.find(DocTableWrapper).length).toBe(1); expect(loadingIndicator.length).toBe(1); }); - it('renders error message', () => { - const props = { ...defaultProps }; - props.anchorStatus = LoadingStatus.FAILED; - const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); - }); - it('should render discover grid correctly', () => { const props = { ...defaultProps, isLegacy: false }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 4d7ce2aa52092..78c354cbf908d 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -10,20 +10,17 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common'; +import { IndexPattern } from '../../../../../data/common'; import { SortDirection } from '../../../../../data/public'; -import { - DocTableLegacy, - DocTableLegacyProps, -} from '../../angular/doc_table/create_doc_table_react'; import { LoadingStatus } from '../../angular/context_query_state'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverGrid } from '../discover_grid/discover_grid'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; -import { EsHitRecord, EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; +import { EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; +import { DocTableContext } from '../../apps/main/components/doc_table/doc_table_context'; export interface ContextAppContentProps { columns: string[]; @@ -44,11 +41,7 @@ export interface ContextAppContentProps { useNewFieldsApi: boolean; isLegacy: boolean; setAppState: (newState: Partial) => void; - addFilter: ( - field: IndexPatternField | string, - values: unknown, - operation: string - ) => Promise; + addFilter: DocViewFilterFn; } const controlColumnIds = ['openDetails']; @@ -57,8 +50,8 @@ export function clamp(value: number) { return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); } -const DataGridMemoized = React.memo(DiscoverGrid); -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DiscoverGridMemoized = React.memo(DiscoverGrid); +const DocTableContextMemoized = React.memo(DocTableContext); const ActionBarMemoized = React.memo(ActionBar); export function ContextAppContent({ @@ -84,8 +77,7 @@ export function ContextAppContent({ }: ContextAppContentProps) { const { uiSettings: config } = services; - const [expandedDoc, setExpandedDoc] = useState(undefined); - const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const [expandedDoc, setExpandedDoc] = useState(); const isAnchorLoading = anchorStatus === LoadingStatus.LOADING || anchorStatus === LoadingStatus.UNINITIALIZED; const arePredecessorsLoading = @@ -100,50 +92,8 @@ export function ContextAppContent({ ); const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]); - const docTableProps = () => { - return { - ariaLabelledBy: 'surDocumentsAriaLabel', - columns, - rows: rows as ElasticSearchHit[], - indexPattern, - expandedDoc, - isLoading: isAnchorLoading, - sampleSize: 0, - sort: sort as [[string, SortDirection]], - isSortEnabled: false, - showTimeCol, - services, - useNewFieldsApi, - isPaginationEnabled: false, - controlColumnIds, - setExpandedDoc, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - onSetColumns, - } as DiscoverGridProps; - }; - - const legacyDocTableProps = () => { - // @ts-expect-error doesn't implement full DocTableLegacyProps interface - return { - columns, - indexPattern, - minimumVisibleRows: rows.length, - rows, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - sort, - useNewFieldsApi, - } as DocTableLegacyProps; - }; - const loadingFeedback = () => { - if ( - isLegacy && - (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) - ) { + if (isLegacy && isAnchorLoading) { return ( @@ -170,18 +120,47 @@ export function ContextAppContent({ docCountAvailable={predecessors.length} onChangeCount={onChangeCount} isLoading={arePredecessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> {loadingFeedback()} - {isLegacy && isAnchorLoaded && ( -
- -
+ {isLegacy && rows && rows.length !== 0 && ( + )} - {!isLegacy && ( + {!isLegacy && rows && rows.length && (
- +
)} @@ -192,7 +171,7 @@ export function ContextAppContent({ docCountAvailable={successors.length} onChangeCount={onChangeCount} isLoading={areSuccessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index f1c56b7a57195..c727e7784cca6 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -28,7 +28,6 @@ import { DiscoverGridFlyout } from './discover_grid_flyout'; import { DiscoverGridContext } from './discover_grid_context'; import { getRenderCellValueFn } from './get_render_cell_value'; import { DiscoverGridSettings } from './types'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; import { getEuiGridColumns, getLeadControlColumns, @@ -40,6 +39,7 @@ import { getDisplayedColumns } from '../../helpers/columns'; import { KibanaContextProvider } from '../../../../../kibana_react/public'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; +import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; interface SortObj { id: string; @@ -369,7 +369,7 @@ export const DiscoverGrid = ({ > {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index 12d56d564b855..e845ba7238303 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -45,7 +45,6 @@ } .kbnDocViewer__value { - display: inline-block; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; diff --git a/src/plugins/discover/public/application/components/table/table_helper.test.ts b/src/plugins/discover/public/application/components/table/table_helper.test.ts deleted file mode 100644 index 738556aaea085..0000000000000 --- a/src/plugins/discover/public/application/components/table/table_helper.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { arrayContainsObjects } from './table_helper'; - -describe('arrayContainsObjects', () => { - it(`returns false for an array of primitives`, () => { - const actual = arrayContainsObjects(['test', 'test']); - expect(actual).toBeFalsy(); - }); - - it(`returns true for an array of objects`, () => { - const actual = arrayContainsObjects([{}, {}]); - expect(actual).toBeTruthy(); - }); - - it(`returns true for an array of objects and primitves`, () => { - const actual = arrayContainsObjects([{}, 'sdf']); - expect(actual).toBeTruthy(); - }); - - it(`returns false for an array of null values`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); - - it(`returns false if no array is given`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); -}); diff --git a/src/plugins/discover/public/application/components/table/table_helper.tsx b/src/plugins/discover/public/application/components/table/table_helper.tsx index 6af349af11f1d..e1c3de8d87c34 100644 --- a/src/plugins/discover/public/application/components/table/table_helper.tsx +++ b/src/plugins/discover/public/application/components/table/table_helper.tsx @@ -6,13 +6,6 @@ * Side Public License, v 1. */ -/** - * Returns true if the given array contains at least 1 object - */ -export function arrayContainsObjects(value: unknown[]): boolean { - return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null); -} - /** * Removes markup added by kibana fields html formatter */ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 58399f31e032f..fe185d7c21f03 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,7 +18,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; +export type ElasticSearchHit = estypes.SearchHit; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index 098c7f55fbd9f..3fd7b2f50d319 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -22,11 +22,10 @@ import { Query, TimeRange, Filter, - IndexPatternField, IndexPattern, ISearchSource, + IndexPatternField, } from '../../../../data/common'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; import { UiActionsStart } from '../../../../ui_actions/public'; @@ -38,23 +37,26 @@ import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; -import * as columnActions from '../angular/doc_table/actions/columns'; -import { getSortForSearchSource, getDefaultSort } from '../angular/doc_table'; +import * as columnActions from '../apps/main/components/doc_table/actions/columns'; import { handleSourceColumnState } from '../angular/helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; - -export interface SearchProps extends Partial { - settings?: DiscoverGridSettings; - description?: string; - sharedItemTitle?: string; - inspectorAdapters?: Adapters; - - filter?: (field: IndexPatternField, value: string[], operator: string) => void; - hits?: ElasticSearchHit[]; - totalHitCount?: number; - onMoveColumn?: (column: string, index: number) => void; -} +import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapper'; +import { getDefaultSort, getSortForSearchSource } from '../apps/main/components/doc_table'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; + +export type SearchProps = Partial & + Partial & { + settings?: DiscoverGridSettings; + description?: string; + sharedItemTitle?: string; + inspectorAdapters?: Adapters; + + filter?: (field: IndexPatternField, value: string[], operator: string) => void; + hits?: ElasticSearchHit[]; + totalHitCount?: number; + onMoveColumn?: (column: string, index: number) => void; + }; interface SearchEmbeddableConfig { savedSearch: SavedSearch; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx index 5b2a2635d04bd..76b316d575cf2 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -8,9 +8,12 @@ import React from 'react'; -import { DiscoverGridEmbeddable } from '../angular/create_discover_grid_directive'; -import { DiscoverDocTableEmbeddable } from '../angular/doc_table/create_doc_table_embeddable'; -import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; +import { + DiscoverGridEmbeddable, + DiscoverGridEmbeddableProps, +} from '../angular/create_discover_grid_directive'; +import { DiscoverDocTableEmbeddable } from '../apps/main/components/doc_table/create_doc_table_embeddable'; +import { DocTableEmbeddableProps } from '../apps/main/components/doc_table/doc_table_embeddable'; import { SearchProps } from './saved_search_embeddable'; interface SavedSearchEmbeddableComponentProps { @@ -32,8 +35,8 @@ export function SavedSearchEmbeddableComponent({ ...searchProps, refs, }; - return ; + return ; } - const discoverGridProps = searchProps as DiscoverGridProps; + const discoverGridProps = searchProps as DiscoverGridEmbeddableProps; return ; } diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover/public/application/embeddable/types.ts index 642c65c4b2a55..5a08534918d4f 100644 --- a/src/plugins/discover/public/application/embeddable/types.ts +++ b/src/plugins/discover/public/application/embeddable/types.ts @@ -12,9 +12,9 @@ import { EmbeddableOutput, IEmbeddable, } from 'src/plugins/embeddable/public'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { Filter, IndexPattern, TimeRange, Query } from '../../../../data/public'; import { SavedSearch } from '../..'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts similarity index 65% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts rename to src/plugins/discover/public/application/helpers/get_single_doc_url.ts index 7eb31459eb4f5..913463e6d44a4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts +++ b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export const truncateByHeight = ({ body }: { body: string }) => { - return `
${body}
`; +export const getSingleDocUrl = (indexPatternId: string, rowIndex: string, rowId: string) => { + return `/app/discover#/doc/${indexPatternId}/${rowIndex}?id=${encodeURIComponent(rowId)}`; }; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index 418cbf6eac9cd..8a28369d1f5f2 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -18,7 +18,7 @@ import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, } from '../angular/context_state'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { getStateColumnActions } from '../apps/main/components/doc_table/actions/columns'; interface UseDataGridColumnsProps { capabilities: Capabilities; diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 7d0228131cc67..09a162e051bf6 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -228,13 +228,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should make the document table scrollable', async function () { await PageObjects.discover.clearFieldSearchInput(); - const dscTable = await find.byCssSelector('.dscTable'); + const dscTableWrapper = await find.byCssSelector('.kbnDocTableWrapper'); const fieldNames = await PageObjects.discover.getAllFieldNames(); - const clientHeight = await dscTable.getAttribute('clientHeight'); + const clientHeight = await dscTableWrapper.getAttribute('clientHeight'); let fieldCounter = 0; const checkScrollable = async () => { - const scrollWidth = await dscTable.getAttribute('scrollWidth'); - const clientWidth = await dscTable.getAttribute('clientWidth'); + const scrollWidth = await dscTableWrapper.getAttribute('scrollWidth'); + const clientWidth = await dscTableWrapper.getAttribute('clientWidth'); log.debug(`scrollWidth: ${scrollWidth}, clientWidth: ${clientWidth}`); return Number(scrollWidth) > Number(clientWidth); }; @@ -251,7 +251,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return await checkScrollable(); }); // so now we need to check if the horizontal scrollbar is displayed - const newClientHeight = await dscTable.getAttribute('clientHeight'); + const newClientHeight = await dscTableWrapper.getAttribute('clientHeight'); expect(Number(clientHeight)).to.be.above(Number(newClientHeight)); }); }); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 8a2a66c41d426..49d56d6f43784 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -202,13 +202,7 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { - basePath = '', - shouldLoginIfPrompted = true, - hash = '', - search = '', - insertTimestamp = true, - } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -217,13 +211,11 @@ export class CommonPageObject extends FtrService { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}${appConfig.pathname}`, hash: hash || appConfig.hash, - search, }); } else { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}/app/${appName}`, hash, - search, }); } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index ecba9549cea02..ea727069c927d 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -228,9 +228,13 @@ export class DashboardPageObject extends FtrService { */ public async expectToolbarPaginationDisplayed() { - const isLegacyDefault = this.discover.useLegacyTable(); + const isLegacyDefault = await this.discover.useLegacyTable(); if (isLegacyDefault) { - const subjects = ['btnPrevPage', 'btnNextPage', 'toolBarPagerText']; + const subjects = [ + 'pagination-button-previous', + 'pagination-button-next', + 'toolBarTotalDocsText', + ]; await Promise.all(subjects.map(async (subj) => await this.testSubjects.existOrFail(subj))); } else { const subjects = ['pagination-button-previous', 'pagination-button-next']; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 0bd873bd7064b..4e6544a20f301 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -11,11 +11,11 @@ import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, HeaderMenuPortal, + SeriesUrl, } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppMountParameters } from '../../../../../../../../src/core/public'; -import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { defaultMessage: 'Analyze data', @@ -38,22 +38,15 @@ export function UXActionMenu({ services: { http }, } = useKibana(); const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom, serviceName } = urlParams; + const { rangeTo, rangeFrom } = urlParams; const uxExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'ux', - name: `${serviceName}-page-views`, - time: { from: rangeFrom!, to: rangeTo! }, - reportDefinitions: { - [SERVICE_NAME]: serviceName ? [serviceName] : [], - }, - selectedMetricField: 'Records', - }, - ], + 'ux-series': ({ + dataType: 'ux', + isNew: true, + time: { from: rangeFrom, to: rangeTo }, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); @@ -67,7 +60,6 @@ export function UXActionMenu({ {ANALYZE_MESSAGE}

}> { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -78,7 +78,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 5af6ea6cdc777..d8ff7fdf47c58 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -9,7 +9,10 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { createExploratoryViewUrl } from '../../../../../../observability/public'; +import { + createExploratoryViewUrl, + SeriesUrl, +} from '../../../../../../observability/public'; import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; import { isIosAgentName, @@ -18,7 +21,6 @@ import { import { SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, @@ -27,11 +29,13 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -function getEnvironmentDefinition(environment: string) { +function getEnvironmentDefinition(environment?: string) { switch (environment) { case ENVIRONMENT_ALL.value: return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; case ENVIRONMENT_NOT_DEFINED.value: + case undefined: + return {}; default: return { [SERVICE_ENVIRONMENT]: [environment] }; } @@ -47,26 +51,21 @@ export function AnalyzeDataButton() { if ( (isRumAgentName(agentName) || isIosAgentName(agentName)) && - rangeFrom && - canShowDashboard && - rangeTo + canShowDashboard ) { const href = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${serviceName}-response-latency`, - selectedMetricField: TRANSACTION_DURATION, - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...(environment ? getEnvironmentDefinition(environment) : {}), - }, - operationType: 'average', + 'apm-series': { + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportType: 'kpi-over-time', + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...getEnvironmentDefinition(environment), }, - ], + operationType: 'average', + isNew: true, + } as SeriesUrl, }, basepath ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 70585f9a9bf28..28fab3369b1eb 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { SERVICE_NAME, @@ -21,10 +20,7 @@ export async function hasRumData({ setup: Setup & Partial; }) { try { - const { - start = moment().subtract(24, 'h').valueOf(), - end = moment().valueOf(), - } = setup; + const { start, end } = setup; const params = { apm: { diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index e10b9e90a71d8..8462f8983a67d 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -111,6 +111,10 @@ export interface ImportResponse { export interface ImportFailure { item: number; reason: string; + caused_by?: { + type: string; + reason: string; + }; doc: ImportDoc; } diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index f93d73647ed0e..deb170974ced8 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -164,6 +164,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { failures.push({ item: i, reason: item.index.error.reason, + caused_by: item.index.error.caused_by, doc: data[i], }); } diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 6a9a4cd9ba83c..716c81573e85a 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -44,7 +44,7 @@ export const getAgentUsage = async ( error, offline, updating, - } = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient); + } = await AgentService.getAgentStatusForAgentPolicy(esClient); return { total_enrolled: total, healthy: online, diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index 9616ba11545e0..47440e791747c 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -76,7 +76,6 @@ export const getFleetServerUsage = async ( } const { total, inactive, online, error, updating, offline } = await getAgentStatusForAgentPolicy( - soClient, esClient, undefined, Array.from(policyIds) diff --git a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index eb70aff3238a0..5f9a4bfde6335 100644 --- a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -36,7 +36,10 @@ export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = { }, }; +export const FLEET_FINAL_PIPELINE_VERSION = 1; +// If the content is updated you probably need to update the FLEET_FINAL_PIPELINE_VERSION too to allow upgrade of the pipeline export const FLEET_FINAL_PIPELINE_CONTENT = `--- +version: ${FLEET_FINAL_PIPELINE_VERSION} description: > Final pipeline for processing all incoming Fleet Agent documents. processors: diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 3aca5e8800dc5..28f3ea96f732e 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -63,4 +63,5 @@ export { FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, FLEET_FINAL_PIPELINE_ID, FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_VERSION, } from './fleet_es_assets'; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index c4ba7e363bc5a..9f07dfac9670b 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -101,6 +101,7 @@ export const createMockAgentPolicyService = (): jest.Mocked => { return { getAgentStatusById: jest.fn(), + getAgentStatusForAgentPolicy: jest.fn(), authenticateAgentWithAccessToken: jest.fn(), getAgent: jest.fn(), listAgents: jest.fn(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 0ab102d91cd4d..94e8032f3375b 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -72,6 +72,7 @@ import { } from './services'; import { getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, getAgentsByKuery, getAgentById, @@ -309,6 +310,7 @@ export class FleetPlugin getAgent: getAgentById, listAgents: getAgentsByKuery, getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, }, agentPolicyService: { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 72a7f4e35ddf5..fd4721309eebb 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -202,13 +202,11 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; try { // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( - soClient, esClient, request.query.policyId, request.query.kuery diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 26cca630f9581..cd8f9b95599b8 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import pMap from 'p-map'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -49,7 +49,6 @@ function joinKuerys(...kuerys: Array) { } export async function getAgentStatusForAgentPolicy( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicyId?: string, filterKuery?: string diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index a6aa87c5ed0f5..46750105900d5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -14,7 +14,11 @@ import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; -import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants'; +import { + FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_ID, + FLEET_FINAL_PIPELINE_VERSION, +} from '../../../../constants'; import { deletePipelineRefs } from './remove'; @@ -195,7 +199,12 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc esClientRequestOptions ); - if (res.statusCode === 404) { + const installedVersion = res?.body[FLEET_FINAL_PIPELINE_ID]?.version; + if ( + res.statusCode === 404 || + !installedVersion || + installedVersion < FLEET_FINAL_PIPELINE_VERSION + ) { await installPipeline({ esClient, pipeline: { diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index f4355320c5a6a..ecef04af6b11e 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -10,6 +10,8 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/ser import type { AgentStatus, Agent } from '../types'; +import type { GetAgentStatusResponse } from '../../common'; + import type { getAgentById, getAgentsByKuery } from './agents'; import type { agentPolicyService } from './agent_policy'; import * as settingsService from './settings'; @@ -56,6 +58,14 @@ export interface AgentService { * Return the status by the Agent's id */ getAgentStatusById(esClient: ElasticsearchClient, agentId: string): Promise; + /** + * Return the status by the Agent's Policy id + */ + getAgentStatusForAgentPolicy( + esClient: ElasticsearchClient, + agentPolicyId?: string, + filterKuery?: string + ): Promise; /** * List agents */ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index ee84fb222bb5c..35fc14e23624f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 7bbbc81b6de7a..0fe4b7805d077 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -19,7 +19,8 @@ "source.ip", "winlog.event_data.LogonType", "user.name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 4b7094e92c6ec..cde52bf7d33cc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index aaee46d9cf80b..2360233937c2b 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -20,7 +20,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index bc08aa21f3277..2a3b4b0100183 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.port" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index d709eb21d7c6d..792d7f2513985 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json index 9ee26b314c640..d2ecf4df624fc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json @@ -19,7 +19,8 @@ "host.name", "user.name", "source.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json index 98d145a91d9a7..b4294227c49d5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json @@ -18,7 +18,8 @@ "aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json index 0332fd53814a6..ad2627ff4f21f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json @@ -34,7 +34,8 @@ "destination.ip", "host.name", "dns.question.etld_plus_one" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json index 613a446750e5f..6fff7246a249a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json @@ -19,7 +19,8 @@ "host.name", "user.name", "winlog.event_data.Path" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index b794f91231505..4273252850da4 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,8 +6,16 @@ }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "observability"], - "optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"], + "configPath": [ + "xpack", + "observability" + ], + "optionalPlugins": [ + "home", + "lens", + "licensing", + "usageCollection" + ], "requiredPlugins": [ "alerting", "cases", diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx deleted file mode 100644 index 0e17c6277618b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function MobileAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { - defaultMessage: 'Add Mobile data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx deleted file mode 100644 index af91624769e6b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function SyntheticsAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { - defaultMessage: 'Add synthetics data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx deleted file mode 100644 index c6aa0742466f1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function UXAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { - defaultMessage: 'Add UX data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx deleted file mode 100644 index 329192abc99d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '../../rtl_helpers'; -import { fireEvent, screen } from '@testing-library/dom'; -import React from 'react'; -import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; -import * as pluginHook from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ExpViewActionMenuContent } from './action_menu'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('Action Menu', function () { - it('should be able to click open in lens', async function () { - const { findByText, core } = render( - - ); - - expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); - - fireEvent.click(await findByText('Open in Lens')); - - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - id: '', - attributes: sampleAttribute, - timeRange: { to: 'now', from: 'now-10m' }, - }, - true - ); - }); - - it('should be able to click save', async function () { - const { findByText } = render( - - ); - - expect(await screen.findByText('Save')).toBeInTheDocument(); - - fireEvent.click(await findByText('Save')); - - expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx deleted file mode 100644 index 38011eb5f8ffb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ObservabilityAppServices } from '../../../../../application/types'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; - -export function ExpViewActionMenuContent({ - timeRange, - lensAttributes, -}: { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -}) { - const kServices = useKibana().services; - - const { lens } = kServices; - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - return ( - <> - - - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - true - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - - - - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - size="s" - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} - - - - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - // if we want to do anything after the viz is saved - // right now there is no action, so an empty function - onSave={() => {}} - /> - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx deleted file mode 100644 index 23500b63e900a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ExpViewActionMenuContent } from './action_menu'; -import HeaderMenuPortal from '../../../header_menu_portal'; -import { usePluginContext } from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; - -interface Props { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} -export function ExpViewActionMenu(props: Props) { - const { appMountParameters } = usePluginContext(); - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index d17e451ef702c..3566835b1701c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_editor/series_editor'; -import { ReportViewType, SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_builder/series_builder'; +import { SeriesUrl } from '../types'; export function EmptyView({ loading, + height, series, - reportType, }: { loading: boolean; - series?: SeriesUrl; - reportType: ReportViewType; + height: string; + series: SeriesUrl; }) { - const { dataType, reportDefinitions } = series ?? {}; + const { dataType, reportType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` text-align: center; + height: ${(props) => props.height}; position: relative; `; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index 03fd23631f755..fe2953edd36d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,10 +27,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -52,10 +51,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -76,10 +74,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -103,10 +100,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index c6254a85de9ac..a08e777c5ea71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -9,24 +9,21 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; -import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string | string[]; - seriesId: number; - series: SeriesUrl; + value: string; + seriesId: string; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; } export function FilterLabel({ label, seriesId, - series, field, value, negate, @@ -34,7 +31,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId, series }); + const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( { - setSeries(seriesId, { ...series, color: colorN }); - }; - - const color = - series.color ?? ((theme.eui as unknown) as Record)[`euiColorVis${seriesId}`]; - - const button = ( - - setIsOpen((prevState) => !prevState)} hasArrow={false}> - - - - ); - - return ( - setIsOpen(false)}> - - - - - ); -} - -const PICK_A_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.pickColor', - { - defaultMessage: 'Pick a color', - } -); - -const EDIT_SERIES_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.editSeriesColor', - { - defaultMessage: 'Edit color for series', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx deleted file mode 100644 index 23d6589fecbcb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx +++ /dev/null @@ -1,109 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { useHasData } from '../../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; -import { parseTimeParts } from '../../series_viewer/columns/utils'; -import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; - -export interface TimePickerTime { - from: string; - to: string; -} - -export interface TimePickerQuickRange extends TimePickerTime { - display: string; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - readonly?: boolean; -} -const readableUnit: Record = { - m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', { - defaultMessage: 'Minutes', - }), - h: i18n.translate('xpack.observability.overview.exploratoryView.hour', { - defaultMessage: 'Hour', - }), - d: i18n.translate('xpack.observability.overview.exploratoryView.day', { - defaultMessage: 'Day', - }), -}; - -export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) { - const { onRefreshTimeRange } = useHasData(); - - const commonlyUsedRanges = useQuickTimeRanges(); - - const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage(); - - function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); - if (reportType === ReportTypes.KPI) { - allSeries.forEach((currSeries, seriesIndex) => { - setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); - }); - } else { - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - } - - const seriesTime = series.time ?? firstSeries!.time; - - const dateFormat = useUiSetting('dateFormat').replace('ss.SSS', 'ss'); - - if (readonly) { - const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to); - - if (timeParts) { - const { - timeTense: timeTenseDefault, - timeUnits: timeUnitsDefault, - timeValue: timeValueDefault, - } = timeParts; - - return ( - {`${timeTenseDefault} ${timeValueDefault} ${ - readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault - }`} - ); - } else { - return ( - - {i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', { - defaultMessage: '{start} to {end}', - values: { - start: moment(seriesTime.from).format(dateFormat), - end: moment(seriesTime.to).format(dateFormat), - }, - })} - - ); - } - } - - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index bf5feb7d5863c..ba1f2214223e3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -94,19 +94,6 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; -export enum ReportTypes { - KPI = 'kpi-over-time', - DISTRIBUTION = 'data-distribution', - CORE_WEB_VITAL = 'core-web-vitals', - DEVICE_DISTRIBUTION = 'device-data-distribution', -} - -export enum DataTypes { - SYNTHETICS = 'synthetics', - UX = 'ux', - MOBILE = 'mobile', -} - export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 55ac75b47c056..6f990015fbc62 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,12 +8,10 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', + REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', - HIDDEN = 'h', - NAME = 'n', - COLOR = 'c', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 3f6551986527c..574a9f6a2bc10 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -15,7 +15,6 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; -import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -25,24 +24,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case DataTypes.UX: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'ux': + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.CORE_WEB_VITAL) { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case DataTypes.SYNTHETICS: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'synthetics': + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case DataTypes.MOBILE: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'mobile': + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 08d2da4714e47..ae70bbdcfa3b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,9 +38,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -53,7 +50,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -66,9 +63,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: RECORDS_FIELD, }, ]); @@ -141,9 +135,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -286,7 +277,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -392,7 +383,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -412,9 +403,6 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -434,7 +422,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -495,7 +483,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -601,9 +589,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5426d3bcd4233..dfb17ee470d35 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; - import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -37,11 +36,10 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, - ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../components/date_range_picker'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -75,6 +73,14 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -89,9 +95,7 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField: string; - color: string; - name: string; + selectedMetricField?: string; } export class LensAttributes { @@ -467,15 +471,14 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time, + time: { from, to }, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - - if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { + if (reportType !== 'kpi-over-time' && totalLayers > 1) { // for kpi over time, we don't need to add time range filters // since those are essentially plotted along the x-axis - baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`; + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -531,11 +534,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if ( - index === 0 || - mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || - !layerConfig.time - ) { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -547,14 +546,11 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); if (inDays > 1) { return inDays + 'd'; } - const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); - if (inHours === 0) { - return null; - } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); return inHours + 'h'; } @@ -572,12 +568,6 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; - let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; - - if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) { - label = layerConfig.name; - } - layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -591,7 +581,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -634,7 +624,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -648,7 +638,7 @@ export class LensAttributes { }; } - getJSON(refresh?: number): TypedLensByValueInput['attributes'] { + getJSON(): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -657,7 +647,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: String(refresh), + description: '', visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 4e178bba7e02a..d1612a08f5551 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DEVICE_DISTRIBUTION, + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - definitionFields: [SERVICE_NAME], metricOptions: [ { - field: 'labels.device_id', id: 'labels.device_id', + field: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 1da27be4fcc95..9b1c4c8da3e9b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 3ee5b3125fcda..945a631078a33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -32,7 +26,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 35e094996f6f2..07bb13f957e45 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,13 +24,10 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, - color: 'green', - name: 'test-series', - breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - time: { from: 'now-15m', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - selectedMetricField: LCP_FIELD, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e8d620388a89e..62455df248085 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,7 +11,6 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, - ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -39,7 +38,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: ReportTypes.CORE_WEB_VITAL, + reportType: 'core-web-vitals', seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -154,6 +153,5 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], - query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index de6f2c67b2aeb..f34c8db6c197d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -46,7 +41,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 9112778eadaa7..5899b16d12b4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -49,7 +43,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d15201..730e742f9d8c5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -35,7 +30,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 65b43a83a8fb5..4ee22181d4334 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index a5898f33e0ec0..569d68ad4ebff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttribute = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -34,23 +28,17 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - dataType: 'number', - isBucketed: true, + sourceField: 'transaction.duration.us', label: 'Page load time', + dataType: 'number', operationType: 'range', + isBucketed: true, + scale: 'interval', params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', }, - scale: 'interval', - sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -60,7 +48,7 @@ export const sampleAttribute = { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -93,16 +81,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - filter: { - language: 'kuery', - query: - 'transaction.type: page-load and processor.event: transaction and transaction.type : *', - }, isBucketed: false, label: 'Part of count() / overall_sum(count())', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, 'y-axis-column-layer0X2': { customLabel: true, @@ -153,51 +141,26 @@ export const sampleAttribute = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: 'transaction.duration.us < 60000000', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 425bf069cc87f..2087b85b81886 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: 'undefined', + description: '', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: 'transaction.type: "page-load"', + query: '', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 85bafdecabde0..7f066caf66bf1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttributeKpi = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -26,27 +20,25 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { + sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { - interval: 'auto', - }, + params: { interval: 'auto' }, scale: 'interval', - sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', - filter: { - language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', - }, isBucketed: false, - label: 'test-series', + label: 'Page views', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, }, }, incompleteColumns: {}, @@ -54,51 +46,26 @@ export const sampleAttributeKpi = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: '', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: '', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 694250e5749cb..f7df2939d9909 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,43 +16,40 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, + reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, - hidden, - name, - color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, + [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, - [URL_KEYS.HIDDEN]: hidden, - [URL_KEYS.NAME]: name, - [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl( - { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, - baseHref = '' -) { - const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); return ( baseHref + - `/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode( - (allShortSeries as unknown) as RisonValue - )}` + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 21c749258bebe..989ebf17c2062 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -11,13 +11,6 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; import * as obsvInd from './utils/observability_index_patterns'; -import * as pluginHook from '../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -48,18 +41,29 @@ describe('ExploratoryView', () => { it('renders exploratory view', async () => { render(); - expect(await screen.findByText(/Preview/i)).toBeInTheDocument(); - expect(await screen.findByText(/Configure series/i)).toBeInTheDocument(); - expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - render(); + const initSeries = { + data: { + 'ux-series': { + isNew: true, + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + breakdown: 'user_agent .name', + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index cb901b8b588f3..af04108c56790 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { useRouteMatch } from 'react-router-dom'; -import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -18,15 +16,40 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesViews } from './views/series_views'; +import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export type PanelId = 'seriesPanel' | 'chartPanel'; +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -46,19 +69,20 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${headerOffset + 40}px)`); + const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; + setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); } }; useEffect(() => { - allSeries.forEach((seriesT) => { + Object.values(allSeries).forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -72,104 +96,38 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); - const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); - - const [hiddenPanel, setHiddenPanel] = useState(''); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const onCollapse = (panelId: string) => { - setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); - }; - - const onChange = (panelId: PanelId) => { - onCollapse(panelId); - if (collapseFn.current) { - collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); - } - }; - return ( {lens ? ( <> - + - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - - {lensAttributes ? ( - - ) : ( - - )} - - - - {!isPreview && - (hiddenPanel === 'chartPanel' ? ( - onChange('chartPanel')} iconType="arrowDown"> - {SHOW_CHART_LABEL} - - ) : ( - onChange('chartPanel')} - iconType="arrowUp" - color="text" - > - {HIDE_CHART_LABEL} - - ))} - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - + {lensAttributes ? ( + + ) : ( + )} + ) : ( -

{LENS_NOT_AVAILABLE}

+

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

)}
@@ -189,39 +147,4 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; - position: relative; -`; - -const ShowPreview = styled(EuiButtonEmpty)` - position: absolute; - bottom: 34px; -`; -const HideChart = styled(EuiButtonEmpty)` - position: absolute; - top: -35px; - right: 50px; `; -const ShowChart = styled(EuiButtonEmpty)` - position: absolute; - top: -10px; - right: 50px; -`; - -const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { - defaultMessage: 'Hide chart', -}); - -const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { - defaultMessage: 'Show chart', -}); - -const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', -}); - -const LENS_NOT_AVAILABLE = i18n.translate( - 'xpack.observability.overview.exploratoryView.lensDisabled', - { - defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1f910b946deb3..8cd8977fcf741 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -8,22 +8,51 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); +import { fireEvent } from '@testing-library/dom'; describe('ExploratoryViewHeader', function () { it('should render properly', function () { const { getByText } = render( ); - getByText('Refresh'); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + const { getByText, core } = render( + , + { initSeries } + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }, + true + ); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bec8673f88b4e..ded56ec9e817f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -5,37 +5,43 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { DataViewLabels } from '../configurations/constants'; +import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { LastUpdated } from './last_updated'; -import { combineTimeRanges } from '../lens_embeddable'; -import { ExpViewActionMenu } from '../components/action_menu'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { - seriesId?: number; - lastUpdated?: number; + seriesId: string; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { - const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const kServices = useKibana().services; - const series = seriesId ? getSeries(seriesId) : undefined; + const { lens } = kServices; - const timeRange = combineTimeRanges(reportType, allSeries, series); + const { getSeries, allSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + const timeRange = combineTimeRanges(allSeries, series); return ( <> -

- {DataViewLabels[reportType] ?? + {DataViewLabels[series.reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -51,18 +57,53 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: - + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + true + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + - setLastRefresh(Date.now())}> - {REFRESH_LABEL} + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + onSave={() => {}} + /> + )} ); } - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index d65917093d129..7a5f12a72b1f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -export type IndexPatternState = Record; +type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx deleted file mode 100644 index e86144c124949..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useState } from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from './use_app_index_pattern'; -import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; -import { getFiltersFromDefs } from './use_lens_attributes'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface UseDiscoverLink { - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { - const kServices = useKibana().services; - const { - application: { navigateToUrl }, - } = kServices; - - const { indexPatterns } = useAppIndexPatternContext(); - - const urlGenerator = kServices.discover?.urlGenerator; - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const indexPattern = indexPatterns?.[series.dataType]; - - const definitions = series.reportDefinitions ?? {}; - const filters = [...(seriesConfig?.baseFilters ?? [])]; - - const definitionFilters = getFiltersFromDefs(definitions); - - definitionFilters.forEach(({ field, values = [] }) => { - if (values.length > 1) { - filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); - } else { - filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); - } - }); - - const selectedMetricField = series.selectedMetricField; - - if ( - selectedMetricField && - selectedMetricField !== RECORDS_FIELD && - selectedMetricField !== RECORDS_PERCENTAGE_FIELD - ) { - filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); - } - - const getDiscoverUrl = async () => { - if (!urlGenerator?.createUrl) return; - - const newUrl = await urlGenerator.createUrl({ - filters, - indexPatternId: indexPattern?.id, - }); - setDiscoverUrl(newUrl); - }; - getDiscoverUrl(); - }, [ - indexPatterns, - series.dataType, - series.reportDefinitions, - series.selectedMetricField, - seriesConfig?.baseFilters, - urlGenerator, - ]); - - const onClick = useCallback( - (event: React.MouseEvent) => { - if (discoverUrl) { - event.preventDefault(); - - return navigateToUrl(discoverUrl); - } - }, - [discoverUrl, navigateToUrl] - ); - - return { - href: discoverUrl, - onClick, - }; -}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 71945734eeabc..8bb265b4f6d89 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -9,18 +9,12 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { - AllSeries, - allSeriesKey, - convertAllShortSeries, - useSeriesStorage, -} from './use_series_storage'; +import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; -import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -34,56 +28,41 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { allSeriesIds, allSeries } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); - const theme = useTheme(); - return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const allSeriesT: AllSeries = autoApply - ? allSeries - : convertAllShortSeries(storage.get(allSeriesKey) ?? []); - const layerConfigs: LayerConfig[] = []; - allSeriesT.forEach((series, seriesIndex) => { - const indexPattern = indexPatterns?.[series?.dataType]; - - if ( - indexPattern && - !isEmpty(series.reportDefinitions) && - !series.hidden && - series.selectedMetricField - ) { + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { const seriesConfig = getDefaultConfigs({ - reportType, + reportType: seriesT.reportType, + dataType: seriesT.dataType, indexPattern, - dataType: series.dataType, }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(series.reportDefinitions) + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions) ); - const color = `euiColorVis${seriesIndex}`; - layerConfigs.push({ filters, indexPattern, seriesConfig, - time: series.time, - name: series.name, - breakdown: series.breakdown, - seriesType: series.seriesType, - operationType: series.operationType, - reportDefinitions: series.reportDefinitions ?? {}, - selectedMetricField: series.selectedMetricField, - color: series.color ?? ((theme.eui as unknown) as Record)[color], + time: seriesT.time, + breakdown: seriesT.breakdown, + seriesType: seriesT.seriesType, + operationType: seriesT.operationType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + selectedMetricField: seriesT.selectedMetricField, }); } }); @@ -94,6 +73,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(lastRefresh); - }, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]); + return lensAttributes.getJSON(); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index f2a6130cdc59d..2d2618bc46152 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,16 +6,18 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { SeriesUrl, UrlFilter } from '../types'; +import { UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string | string[]; + value: string; negate?: boolean; } -export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { - const { setSeries } = useSeriesStorage(); +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const filters = series.filters ?? []; @@ -24,14 +26,10 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValuesN = filter.notValues?.filter((val) => val !== value); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const valuesN = filter.values?.filter((val) => val !== value); return { ...filter, values: valuesN }; } } @@ -45,9 +43,9 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = value instanceof Array ? value : [value]; + currFilter.notValues = [value]; } else { - currFilter.values = value instanceof Array ? value : [value]; + currFilter.values = [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -67,26 +65,13 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); - - const values = currValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); if (negate) { - if (value instanceof Array) { - notValues.push(...value); - } else { - notValues.push(value); - } + notValues.push(value); } else { - if (value instanceof Array) { - values.push(...value); - } else { - values.push(value); - } + values.push(value); } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ce6d7bd94d8e4..c32acc47abd1b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,39 +6,37 @@ */ import React, { useEffect } from 'react'; -import { Route, Router } from 'react-router-dom'; -import { render } from '@testing-library/react'; + import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; -import { getHistoryFromUrl } from '../rtl_helpers'; +import { render } from '@testing-library/react'; -const mockSingleSeries = [ - { - name: 'performance-distribution', +const mockSingleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -const mockMultipleSeries = [ - { - name: 'performance-distribution', +const mockMultipleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -describe('userSeriesStorage', function () { +describe('userSeries', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); - function TestComponent() { const data = useSeriesStorage(); @@ -50,20 +48,11 @@ describe('userSeriesStorage', function () { } render( - - - (key === 'sr' ? seriesData : null)), - set: jest.fn(), - }} - > - - - - + + + ); return setData; @@ -74,20 +63,22 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); @@ -98,38 +89,42 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution', 'kpi-over-time'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent([]); + const setData = setupTestComponent({}); - expect(setData).toHaveBeenCalledTimes(1); + expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [], + allSeries: {}, + allSeriesIds: [], firstSeries: undefined, + firstSeriesId: undefined, }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 04f8751e2a0b6..a47a124d14b4d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -6,7 +6,6 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { IKbnUrlStateStorage, ISessionStorageStateStorage, @@ -23,19 +22,13 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries?: SeriesUrl; - autoApply: boolean; - lastRefresh: number; - setLastRefresh: (val: number) => void; - setAutoApply: (val: boolean) => void; - applyChanges: () => void; + firstSeries: SeriesUrl; + firstSeriesId: string; + allSeriesIds: string[]; allSeries: AllSeries; - setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; - getSeries: (seriesIndex: number) => SeriesUrl | undefined; - removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; - storage: IKbnUrlStateStorage | ISessionStorageStateStorage; - reportType: ReportViewType; + setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; + getSeries: (seriesId: string) => SeriesUrl; + removeSeries: (seriesId: string) => void; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -43,112 +36,72 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -export function convertAllShortSeries(allShortSeries: AllShortSeries) { - return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); -} +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); -export const allSeriesKey = 'sr'; -const autoApplyKey = 'autoApply'; -const reportTypeKey = 'reportType'; + return allSeriesN; +} export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? []) - ); - - const [autoApply, setAutoApply] = useState(() => storage.get(autoApplyKey) ?? true); - const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + const allSeriesKey = 'sr'; - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [allShortSeries, setAllShortSeries] = useState( + () => storage.get(allSeriesKey) ?? {} ); - + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); + const [firstSeriesId, setFirstSeriesId] = useState(''); const [firstSeries, setFirstSeries] = useState(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - useEffect(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - const firstSeriesT = allSeries?.[0]; - - setFirstSeries(firstSeriesT); - - if (autoApply) { - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - } - }, [allSeries, autoApply, storage]); useEffect(() => { - // needed for tab change - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + setAllSeries(allSeriesN); + setFirstSeriesId(allSeriesIds?.[0]); + setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - // this is only needed for tab change, so we will not add allSeries into dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPreview, storage]); - - const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { - setAllSeries((prevAllSeries) => { - const newStateRest = prevAllSeries.map((series, index) => { - if (index === seriesIndex) { - return newValue; - } - return series; - }); - - if (prevAllSeries.length === seriesIndex) { - return [...newStateRest, newValue]; - } - - return [...newStateRest]; + }, [allShortSeries, storage]); + + const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { + setAllShortSeries((prevState) => { + prevState[seriesIdN] = convertToShortUrl(newValue); + return { ...prevState }; }); - }, []); + }; - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); + const removeSeries = (seriesIdN: string) => { + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); + }; - const removeSeries = useCallback((seriesIndex: number) => { - setAllSeries((prevAllSeries) => - prevAllSeries.filter((seriesT, index) => index !== seriesIndex) - ); - }, []); + const allSeriesIds = Object.keys(allShortSeries); const getSeries = useCallback( - (seriesIndex: number) => { - return allSeries[seriesIndex]; + (seriesId?: string) => { + return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); }, [allSeries] ); - const applyChanges = useCallback(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - setLastRefresh(Date.now()); - }, [allSeries, storage]); - - useEffect(() => { - (storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply); - }, [autoApply, storage]); - const value = { - autoApply, - setAutoApply, - applyChanges, storage, getSeries, setSeries, removeSeries, + firstSeriesId, allSeries, - lastRefresh, - setLastRefresh, - setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + allSeriesIds, firstSeries: firstSeries!, }; return {children}; @@ -159,9 +112,10 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, + reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -169,31 +123,26 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, - hidden: h, - name: n, - color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; - [URL_KEYS.HIDDEN]?: boolean; - [URL_KEYS.NAME]: string; - [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = ShortUrlSeries[]; -export type AllSeries = SeriesUrl[]; +export type AllShortSeries = Record; +export type AllSeries = Record; -export const NEW_SERIES_KEY = 'new-series'; +export const NEW_SERIES_KEY = 'new-series-key'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..e55752ceb62ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 9e4d9486dc155..4cb586fe94ceb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,51 +7,16 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { combineTimeRanges } from './exploratory_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ReportViewType, SeriesUrl } from './types'; -import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } -export const combineTimeRanges = ( - reportType: ReportViewType, - allSeries: SeriesUrl[], - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - - if (reportType === ReportTypes.KPI) { - return firstSeries?.time; - } - - allSeries.forEach((series) => { - if ( - series.dataType && - series.selectedMetricField && - !isEmpty(series.reportDefinitions) && - series.time - ) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - - return { to, from }; -}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -62,11 +27,9 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); + const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); - const firstSeriesId = 0; - - const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; + const timeRange = combineTimeRanges(allSeries, series); const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -74,9 +37,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (reportType !== 'data-distribution' && firstSeries) { + if (series?.reportType !== 'data-distribution') { setSeries(firstSeriesId, { - ...firstSeries, + ...series, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -90,30 +53,16 @@ export function LensEmbeddable(props: Props) { ); } }, - [reportType, setSeries, firstSeries, notifications?.toasts] + [notifications?.toasts, series, firstSeriesId, setSeries] ); - if (timeRange === null || !firstSeries) { - return null; - } - return ( - - - + ); } - -const LensWrapper = styled.div` - height: 100%; - - &&& > div { - height: 100%; - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 0e609cbe6c9e5..972e3beb4b722 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Route, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -39,10 +39,9 @@ import { IndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { AppDataType, SeriesUrl, UrlFilter } from './types'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; -import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; interface KibanaProps { services?: KibanaServices; @@ -159,11 +158,9 @@ export function MockRouter({ }: MockRouterProps) { return ( - - - {children} - - + + {children} + ); } @@ -176,7 +173,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url = '/app/observability/exploratory-view/configure#?autoApply=!t', + url, initSeries = {}, }: RenderRouterOptions = {} ) { @@ -206,7 +203,7 @@ export function render( }; } -export const getHistoryFromUrl = (url: Url) => { +const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -255,15 +252,6 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; -export const mockUxSeries = { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - reportDefinitions: { 'service.name': ['elastic-co'] }, - selectedMetricField: TRANSACTION_DURATION, -} as SeriesUrl; - function mockSeriesStorageContext({ data, filters, @@ -273,34 +261,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const testSeries = { - ...mockUxSeries, - breakdown: breakdown || 'user_agent.name', - ...(filters ? { filters } : {}), + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'data-distribution', + dataType: 'ux', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; - const mockDataSeries = data || [testSeries]; + const series = mockDataSeries[firstSeriesId]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(testSeries); + const getSeries = jest.fn().mockReturnValue(series); return { + firstSeriesId, + allSeriesIds, removeSeries, setSeries, getSeries, - autoApply: true, - reportType: 'data-distribution', - lastRefresh: Date.now(), - setLastRefresh: jest.fn(), - setAutoApply: jest.fn(), - applyChanges: jest.fn(), - firstSeries: mockDataSeries[0], + firstSeries: mockDataSeries[firstSeriesId], allSeries: mockDataSeries, - setReportType: jest.fn(), - storage: { get: jest.fn() } as any, - } as SeriesContextValue; + }; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx index 8f196b8a05dda..c054853d9c877 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 27d846502dbe6..50c2f91e6067d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; +import { useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,14 +20,16 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - series, + seriesTypes, defaultChartType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; + seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; @@ -40,15 +42,17 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={[ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ]} + includeChartTypes={ + seriesTypes || [ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ] + } label={CHART_TYPE_LABEL} /> ); @@ -101,14 +105,14 @@ export function XYChartTypesSelect({ }); return ( - - - + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx new file mode 100644 index 0000000000000..b10702ebded57 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; + +describe('DataTypesCol', function () { + const seriesId = 'test-series-id'; + + mockAppIndexPattern(); + + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = render(); + + fireEvent.click(screen.getByText(/user experience \(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); + + it('should set series on change on already selected', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx new file mode 100644 index 0000000000000..f386f62d9ed73 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'ux', label: 'User Experience (RUM)' }, + { id: 'mobile', label: 'Mobile Experience' }, + // { id: 'infra_logs', label: 'Logs' }, + // { id: 'infra_metrics', label: 'Metrics' }, + // { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol({ seriesId }: { seriesId: string }) { + const { getSeries, setSeries, removeSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { loading } = useAppIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (!dataType) { + removeSeries(seriesId); + } else { + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); + } + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + + + ))} + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx new file mode 100644 index 0000000000000..6be78084ae195 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + + return ( + + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} + + ); +} + +const Wrapper = styled.div` + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0px; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index ced4d3af057ff..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -7,66 +7,62 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'performance-distribution': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - render(, { - initSeries, - }); + render(, { initSeries }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'series-id': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - const { setSeries } = render(, { - initSeries, - }); + const { setSeries } = render(, { initSeries }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx index 4c10c9311704d..fce1383f30f34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -11,18 +11,17 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; -import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, - series, defaultOperationType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; defaultOperationType?: OperationType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const operationType = series?.operationType; @@ -84,7 +83,11 @@ export function OperationTypeSelect({ return ( ); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: USER_AGENT_OS, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: undefined, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 0000000000000..fa2d01691ce1d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { SeriesConfig } from '../../types'; + +export function ReportBreakdowns({ + seriesId, + seriesConfig, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx similarity index 65% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 544a294e021e2..3d156e0ee9c2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, - mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 0; + const seriesId = 'test-series-id'; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,24 +27,36 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); + const initSeries = { + data: { + [seriesId]: { + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + }, + }, + }; + mockUseValuesList([{ label: 'elastic-co', count: 10 }]); - it('renders', async () => { - render( - - ); + it('should render properly', async function () { + render(, { + initSeries, + }); await waitFor(() => { - expect(screen.getByText('Web Application')).toBeInTheDocument(); - expect(screen.getByText('Environment')).toBeInTheDocument(); - expect(screen.getByText('Search Environment')).toBeInTheDocument(); + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); }); }); it('should render selected report definitions', async function () { - render( - - ); + render(, { + initSeries, + }); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -53,7 +65,8 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - + , + { initSeries } ); expect( @@ -67,14 +80,11 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', - name: 'performance-distribution', - breakdown: 'user_agent.name', reportDefinitions: {}, - selectedMetricField: 'transaction.duration.us', - time: { from: 'now-15m', to: 'now' }, + reportType: 'data-distribution', + time: { from: 'now-30d', to: 'now' }, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx new file mode 100644 index 0000000000000..0c620abf56e8a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import styled from 'styled-components'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; +import { OperationTypeSelect } from './operation_type_select'; +import { DatePickerCol } from './date_picker_col'; +import { parseCustomFieldName } from '../../configurations/lens_attributes'; +import { ReportDefinitionField } from './report_definition_field'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +export function ReportDefinitionCol({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; + + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( + + + + + + {definitionFields.map((field) => ( + + + + ))} + {metricOptions && ( + + + + )} + {(hasOperationType || columnType === 'operation') && ( + + + + )} + + + + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 3651b4b7f075b..8a83b5c2a8cb0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -6,25 +6,30 @@ */ import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -59,26 +64,23 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); - if (!indexPattern) { - return null; - } - return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - asCombobox={true} - allowExclusions={false} - allowAllValuesSelection={true} - usePrependLabel={false} - compressed={false} - required={isEmpty(selectedReportDefinitions)} - /> + + + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + allowAllValuesSelection={true} + /> + )} + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx new file mode 100644 index 0000000000000..0b183b5f20c03 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, render } from '../../rtl_helpers'; + +describe('Series Builder ReportFilters', function () { + const seriesId = 'test-series-id'; + + const dataViewSeries = getDefaultConfigs({ + reportType: 'data-distribution', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 0000000000000..d5938c5387e8f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { SeriesConfig } from '../../types'; + +export function ReportFilters({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx new file mode 100644 index 0000000000000..12ae8560453c9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; +import { ReportTypes } from '../series_builder'; +import { DEFAULT_TIME } from '../../configurations/constants'; + +describe('ReportTypesCol', function () { + const seriesId = 'performance-distribution'; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = render( + + ); + + fireEvent.click(screen.getByText(/KPI over time/i)); + + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + selectedMetricField: undefined, + reportType: 'kpi-over-time', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + isNew: true, + }, + }, + }; + + const { setSeries } = render( + , + { initSeries } + ); + + const button = screen.getByRole('button', { + name: /KPI over time/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'synthetics', + time: DEFAULT_TIME, + isNew: true, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx new file mode 100644 index 0000000000000..c4eebbfaca3eb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; +import { ReportViewType, SeriesUrl } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { DEFAULT_TIME } from '../../configurations/constants'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem } from '../series_builder'; + +interface Props { + seriesId: string; + reportTypes: ReportTypeItem[]; +} + +export function ReportTypesCol({ seriesId, reportTypes }: Props) { + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); + + const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); + + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); + + if (!restSeries.dataType) { + return ( + + ); + } + + if (!loading && !hasData) { + return ( + + ); + } + + const disabledReportTypes: ReportViewType[] = map( + reportTypes.filter( + ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ reportType, label }) => ( + + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'No data type selected.' } +); + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx index c352ec0423dd8..874171de123d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; -import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -19,34 +18,20 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 5000); + }, 1000); return () => { clearInterval(interVal); }; }, []); - useEffect(() => { - setRefresh(Date.now()); - }, [lastUpdated]); - if (!lastUpdated) { return null; } - const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; - const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; - return ( - - - + + Last Updated: {moment(lastUpdated).from(refresh)} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx new file mode 100644 index 0000000000000..a2a3e34c21834 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig } from '../types'; + +interface Props { + seriesId: string; + defaultValue?: string; + options: SeriesConfig['metricOptions']; +} + +export function ReportMetricOptions({ seriesId, options: opts }: Props) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const onChange = (value: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + const options = opts ?? []; + + return ( + ({ + value: fd || id, + inputDisplay: label, + }))} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} + onChange={(value) => onChange(value)} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 0000000000000..684cf3a210a51 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,303 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { RefObject, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesActions } from '../series_editor/columns/series_actions'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LastUpdated } from './last_updated'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../configurations/constants/labels'; + +export interface ReportTypeItem { + id: string; + reportType: ReportViewType; + label: string; +} + +export const ReportTypes: Record = { + synthetics: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + ], + ux: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + ], + mobile: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, + ], + apm: [], + infra_logs: [], + infra_metrics: [], +}; + +interface BuilderItem { + id: string; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesBuilder({ + seriesBuilderRef, + lastUpdated, + multiSeries, +}: { + seriesBuilderRef: RefObject; + lastUpdated?: number; + multiSeries?: boolean; +}) { + const [editorItems, setEditorItems] = useState([]); + const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + indexPattern: indexPatterns[dataType], + reportType: reportType!, + }); + } + }; + + const seriesToEdit: BuilderItem[] = + allSeriesIds + .filter((sId) => { + return allSeries?.[sId]?.isNew; + }) + .map((sId) => { + const series = getSeries(sId); + const seriesConfig = getDataViewSeries(series.dataType, series.reportType); + + return { id: sId, series, seriesConfig }; + }) ?? []; + const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; + setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); + }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + field: 'id', + width: '15%', + render: (seriesId: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '15%', + field: 'id', + render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + field: 'id', + render: ( + seriesId: string, + { series: { dataType, reportType }, seriesConfig }: BuilderItem + ) => { + if (dataType && seriesConfig) { + return loading ? ( + LOADING_VIEW + ) : reportType ? ( + + ) : ( + SELECT_REPORT_TYPE + ); + } + + return null; + }, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + ...(multiSeries + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: BuilderItem) => ( + + ), + }, + ] + : []), + ]; + + const applySeries = () => { + editorItems.forEach(({ series, id: seriesId }) => { + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + + if (reportType && !isEmpty(reportDefinitions)) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + const newSeriesId = `${reportDefId}-${reportType}`; + + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }); + }; + + const addSeries = () => { + const prevSeries = allSeries?.[allSeriesIds?.[0]]; + setSeries( + `${NEW_SERIES_KEY}-${editorItems.length + 1}`, + prevSeries + ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) + : ({ isNew: true } as SeriesUrl) + ); + }; + + return ( + + {multiSeries && ( + + + + + + {}} + compressed + /> + + + applySeries()} isDisabled={true} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + + addSeries()} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add Series', + })} + + + + )} +
+ {multiSeries && } + {editorItems.length > 0 && ( + + )} + +
+
+ ); +} + +const Wrapper = euiStyled.div` + max-height: 50vh; + overflow-y: scroll; + overflow-x: clip; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx index 0b8e1c1785c7f..c30863585b3b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { Moment } from 'moment'; import DateMath from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; +import { Moment } from 'moment'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../types'; -import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { - const { firstSeries, setSeries, reportType } = useSeriesStorage(); +export function DateRangePicker({ seriesId }: { seriesId: string }) { + const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const seriesFrom = series.time?.from; - const seriesTo = series.time?.to; + const { + time: { from, to }, + reportType, + } = getSeries(firstSeriesId); - const { from: mainFrom, to: mainTo } = firstSeries!.time; + const series = getSeries(seriesId); - const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; - const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; + const { + time: { from: seriesFrom, to: seriesTo }, + } = series; - const getTotalDuration = () => { - const mainStartDate = parseAbsoluteDate(mainTo)!; - const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; - return mainEndDate.diff(mainStartDate, 'millisecond'); - }; + const startDate = parseAbsoluteDate(seriesFrom ?? from)!; + const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; - const onStartChange = (newStartDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newFrom = newStartDate.toISOString(); - const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newFrom = newDate.toISOString(); + const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newStartDate.toISOString(); + const newFrom = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,19 +55,20 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series }); } }; - - const onEndChange = (newEndDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newTo = newEndDate.toISOString(); - const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); + const onEndChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newTo = newDate.toISOString(); + const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newEndDate.toISOString(); + const newTo = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -89,7 +90,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } @@ -103,7 +104,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 0000000000000..e21da424b58c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; +import { DEFAULT_TIME } from '../configurations/constants'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: DEFAULT_TIME }); + } + }, [series, seriesId, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx similarity index 50% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 3517508300e4b..931dfbe07cd23 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -6,48 +6,67 @@ */ import React from 'react'; -import { mockUseHasData, render } from '../../rtl_helpers'; +import { mockUseHasData, render } from '../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; +import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; - const { getByText } = render(, { - initSeries, - }); + const { getByText } = render(, { initSeries }); - getByText('Last 30 Minutes'); + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + reportType: 'kpi-over-time' as const, + dataType: 'synthetics' as const, + breakdown: 'monitor.status', + }, + }, + }; + const { setSeries: setSeries1 } = render( + , + { initSeries: initSeries as any } + ); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + time: DEFAULT_TIME, + }); }); it('should set series data', async function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render( - , - { - initSeries, - } - ); + const { getByTestId, setSeries } = render(, { + initSeries, + }); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -57,10 +76,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(0, { - name: 'uptime-pings-histogram', + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', + reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx new file mode 100644 index 0000000000000..207a53e13f1ad --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Breakdowns } from './columns/breakdowns'; +import { SeriesConfig } from '../types'; +import { ChartOptions } from './columns/chart_options'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; + breakdownFields: string[]; +} +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 21b766227a562..84568e1c5068a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { mockIndexPattern, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,7 +20,13 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render(); + render( + + ); screen.getAllByText('Browser family'); }); @@ -30,9 +36,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -43,14 +49,10 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - name: 'performance-distribution', - reportDefinitions: { - 'service.name': ['elastic-co'], - }, - selectedMetricField: 'transaction.duration.us', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 315f63e33bed0..2237935d466ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useRouteMatch } from 'react-router-dom'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; + breakdowns: string[]; seriesConfig: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, series }: Props) { - const { setSeries } = useSeriesStorage(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { + const { setSeries, getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,13 +40,9 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; - if (!seriesConfig) { - return null; - } - const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = seriesConfig.breakdownFields.map((breakdown) => ({ + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -54,12 +50,14 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: NO_BREAK_DOWN_LABEL, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), }); } const options = items.map(({ id, label }) => ({ - inputDisplay: label, + inputDisplay: id === NO_BREAKDOWN ? label : {label}, value: id, dropdownDisplay: label, })); @@ -71,7 +69,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
onOptionChange(value)} @@ -80,10 +78,3 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
); } - -export const NO_BREAK_DOWN_LABEL = i18n.translate( - 'xpack.observability.exp.breakDownFilter.noBreakdown', - { - defaultMessage: 'No breakdown', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx new file mode 100644 index 0000000000000..f2a6377fd9b71 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesConfig } from '../../types'; +import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; +import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; +} + +export function ChartOptions({ seriesConfig, seriesId }: Props) { + return ( + + + + + {seriesConfig.hasOperationType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx deleted file mode 100644 index 838631e1f05df..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; -import { DataTypesLabels, DataTypesSelect } from './data_type_select'; -import { DataTypes } from '../../configurations/constants'; - -describe('DataTypeSelect', function () { - const seriesId = 0; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - }); - - it('should set series on change', async function () { - const { setSeries } = render(); - - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX])); - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - name: 'synthetics-series-1', - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx deleted file mode 100644 index b0a6e3b5e26b0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx +++ /dev/null @@ -1,105 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { AppDataType, SeriesUrl } from '../../types'; -import { DataTypes, ReportTypes } from '../../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export const DataTypesLabels = { - [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { - defaultMessage: 'User experience (RUM)', - }), - - [DataTypes.SYNTHETICS]: i18n.translate( - 'xpack.observability.overview.exploratoryView.syntheticsLabel', - { - defaultMessage: 'Synthetics monitoring', - } - ), - - [DataTypes.MOBILE]: i18n.translate( - 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', - { - defaultMessage: 'Mobile experience', - } - ), -}; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { - id: DataTypes.SYNTHETICS, - label: DataTypesLabels[DataTypes.SYNTHETICS], - }, - { - id: DataTypes.UX, - label: DataTypesLabels[DataTypes.UX], - }, - { - id: DataTypes.MOBILE, - label: DataTypesLabels[DataTypes.MOBILE], - }, -]; - -const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; - -export function DataTypesSelect({ seriesId, series }: Props) { - const { setSeries, reportType } = useSeriesStorage(); - - const onDataTypeChange = (dataType: AppDataType) => { - if (String(dataType) !== SELECT_DATA_TYPE) { - setSeries(seriesId, { - dataType, - time: series.time, - name: `${dataType}-series-${seriesId + 1}`, - }); - } - }; - - const options = dataTypes - .filter(({ id }) => { - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { - return id === DataTypes.MOBILE; - } - if (reportType === ReportTypes.CORE_WEB_VITAL) { - return id === DataTypes.UX; - } - return true; - }) - .map(({ id, label }) => ({ - value: id, - inputDisplay: label, - })); - - return ( - onDataTypeChange(value as AppDataType)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.selectDataType', - { - defaultMessage: 'Select data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 032eb66dcfa4f..41e83f407af2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,84 +6,24 @@ */ import React from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesDatePicker } from '../../series_date_picker'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../components/date_range_picker'; -import { SeriesDatePicker } from '../../components/series_date_picker'; -import { AppDataType, SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; -import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; -import { UXAddData } from '../../../add_data_buttons/ux_add_data'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; } - -const AddDataComponents: Record = { - mobile: MobileAddData, - ux: UXAddData, - synthetics: SyntheticsAddData, - apm: null, - infra_logs: null, - infra_metrics: null, -}; - -export function DatePickerCol({ seriesId, series }: Props) { - const { reportType } = useSeriesStorage(); - - const { hasAppData } = useAppIndexPatternContext(); - - if (!series.dataType) { - return null; - } - - const AddDataButton = AddDataComponents[series.dataType]; - if (hasAppData[series.dataType] === false && AddDataButton !== null) { - return ( - - - - {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { - defaultMessage: 'No {dataType} data available.', - values: { - dataType: series.dataType, - }, - })} - - - - - - - ); - } - - if (!series.selectedMetricField) { - return null; - } +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); return ( - - {seriesId === 0 || reportType !== ReportTypes.KPI ? ( - +
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + ) : ( - + )} - +
); } - -const Wrapper = styled.div` - width: 100%; - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx similarity index 67% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index a88e2eadd10c9..90a039f6b44d0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -8,24 +8,20 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - - const mockSeries = { ...mockUxSeries, filters }; - - it('render', async () => { - const initSeries = { filters }; + it('should render properly', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockAppIndexPattern(); render( , { initSeries } @@ -37,14 +33,15 @@ describe('FilterExpanded', function () { }); it('should call go back on click', async function () { - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const goBack = jest.fn(); render( , { initSeries } @@ -52,23 +49,28 @@ describe('FilterExpanded', function () { await waitFor(() => { fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); }); }); - it('calls useValuesList on load', async () => { - const initSeries = { filters }; + it('should call useValuesList on load', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); + const goBack = jest.fn(); + render( , { initSeries } @@ -85,8 +87,8 @@ describe('FilterExpanded', function () { }); }); - it('filters display values', async () => { - const initSeries = { filters }; + it('should filter display values', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -95,20 +97,18 @@ describe('FilterExpanded', function () { render( , { initSeries } ); - await waitFor(() => { - fireEvent.click(screen.getByText('Browser Family')); - - expect(screen.queryByText('Firefox')).toBeTruthy(); + expect(screen.getByText('Firefox')).toBeTruthy(); + await waitFor(() => { fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); expect(screen.queryByText('Firefox')).toBeFalsy(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 1ef25722aff5c..4310402a43a08 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,14 +6,7 @@ */ import React, { useState, Fragment } from 'react'; -import { - EuiFieldSearch, - EuiSpacer, - EuiFilterGroup, - EuiText, - EuiPopover, - EuiFilterButton, -} from '@elastic/eui'; +import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; @@ -21,7 +14,8 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,33 +23,31 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; label: string; field: string; isNegated?: boolean; + goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } -export interface NestedFilterOpen { - value: string; - negate: boolean; -} - export function FilterExpanded({ seriesId, - series, field, label, + goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const queryFilters: ESFilter[] = []; @@ -89,71 +81,62 @@ export function FilterExpanded({ ); return ( - setIsOpen((prevState) => !prevState)} iconType="arrowDown"> - {label} - - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( - - )} + + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( - - - - ))} - - - + )} + + + + + ))} + +
); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx similarity index 64% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 764a27fd663f5..a9609abc70d69 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,98 +19,84 @@ describe('FilterValueButton', function () { render( ); - await waitFor(() => { - expect(screen.getByText('Chrome')).toBeInTheDocument(); - }); + screen.getByText('Chrome'); }); - describe('when negate is true', () => { - it('displays negate stats', async () => { - render( - - ); + it('should render display negate state', async function () { + render( + + ); - await waitFor(() => { - expect(screen.getByText('Not Chrome')).toBeInTheDocument(); - expect(screen.getByTitle('Not Chrome')).toBeInTheDocument(); - const btn = screen.getByRole('button'); - expect(btn.classList).toContain('euiButtonEmpty--danger'); - }); + await waitFor(() => { + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); }); + }); - it('calls setFilter on click', async () => { - const { setFilter, removeFilter } = mockUseSeriesFilter(); + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); - render( - - ); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Not Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledTimes(0); - expect(setFilter).toHaveBeenCalledTimes(1); - - expect(setFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: true, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', }); }); }); - describe('when selected', () => { - it('removes the filter on click', async () => { - const { removeFilter } = mockUseSeriesFilter(); - - render( - - ); + it('should remove filter on click if already selected', async function () { + const { removeFilter } = mockUseSeriesFilter(); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: false, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', }); }); }); @@ -121,13 +107,12 @@ describe('FilterValueButton', function () { render( ); @@ -149,14 +134,13 @@ describe('FilterValueButton', function () { render( ); @@ -183,14 +167,13 @@ describe('FilterValueButton', function () { render( ); @@ -220,14 +203,13 @@ describe('FilterValueButton', function () { render( ); @@ -247,14 +229,13 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 111f915a95f46..bf4ca6eb83d94 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { SeriesUrl } from '../../types'; -import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -20,13 +19,12 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: number; - series: SeriesUrl; + seriesId: string; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: NestedFilterOpen) => void; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; } export function FilterValueButton({ @@ -36,13 +34,16 @@ export function FilterValueButton({ field, negate, seriesId, - series, nestedField, allSelectedValues, }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 0000000000000..e75f308dab1e5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} + +export function RemoveSeries({ seriesId }: Props) { + const { removeSeries } = useSeriesStorage(); + + const onClick = () => { + removeSeries(seriesId); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx deleted file mode 100644 index dad2a7da2367b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { ReportDefinitionField } from './report_definition_field'; - -export function ReportDefinitionCol({ - seriesId, - series, - seriesConfig, -}: { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -}) { - const { setSeries } = useSeriesStorage(); - - const { reportDefinitions: selectedReportDefinitions = {} } = series; - - const { definitionFields } = seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - return ( - - {definitionFields.map((field) => ( - - - - ))} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx deleted file mode 100644 index 01c9fce7637bb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportViewType } from '../../types'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../../configurations/constants/labels'; - -const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; - -export const reportTypesList: Array<{ - reportType: ReportViewType | typeof SELECT_REPORT_TYPE; - label: string; -}> = [ - { - reportType: SELECT_REPORT_TYPE, - label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { - defaultMessage: 'Select report type', - }), - }, - { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, -]; - -export function ReportTypesSelect() { - const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); - - const onReportTypeChange = (reportType: ReportViewType) => { - setReportType(reportType); - }; - - const options = reportTypesList - .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) - .map(({ reportType, label }) => ({ - value: reportType, - inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, - dropdownDisplay: label, - })); - - return ( - onReportTypeChange(value as ReportViewType)} - style={{ minWidth: 200 }} - isInvalid={!selectedReportType && allSeries.length > 0} - disabled={allSeries.length > 0} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx new file mode 100644 index 0000000000000..51ebe6c6bd9d5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { RemoveSeries } from './remove_series'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: string; + editorMode?: boolean; +} +export function SeriesActions({ seriesId, editorMode = false }: Props) { + const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); + const series = getSeries(seriesId); + + const onEdit = () => { + setSeries(seriesId, { ...series, isNew: true }); + }; + + const copySeries = () => { + let copySeriesId: string = `${seriesId}-copy`; + if (allSeriesIds.includes(copySeriesId)) { + copySeriesId = copySeriesId + allSeriesIds.length; + } + setSeries(copySeriesId, series); + }; + + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + const isSaveAble = reportType && !isEmpty(reportDefinitions); + + const saveSeries = () => { + if (isSaveAble) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + let newSeriesId = `${reportDefId}-${reportType}`; + + if (allSeriesIds.includes(newSeriesId)) { + newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; + } + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }; + + return ( + + {!editorMode && ( + + + + )} + {editorMode && ( + + + + )} + {editorMode && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 0000000000000..02144c6929b38 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { SeriesConfig } from '../../types'; +import { FieldLabels } from '../../configurations/constants/constants'; +import { SelectedFilters } from '../selected_filters'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; + isNew?: boolean; + labels?: Record; +} + +export interface Field { + label: string; + field: string; + nested?: string; + isNegated?: boolean; +} + +export function SeriesFilter({ + seriesConfig, + isNew, + seriesId, + filterFields = [], + baseFilters, + labels, +}: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options: Field[] = filterFields.map((field) => { + if (typeof field === 'string') { + return { label: labels?.[field] ?? FieldLabels[field], field }; + } + + return { + field: field.field, + nested: field.nested, + isNegated: field.isNegated, + label: labels?.[field.field] ?? FieldLabels[field.field], + }; + }); + + const { setSeries, getSeries } = useSeriesStorage(); + const urlSeries = getSeries(seriesId); + + const button = ( + { + setIsPopoverVisible((prevState) => !prevState); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + filters={baseFilters} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx deleted file mode 100644 index 801c885ec9a62..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ /dev/null @@ -1,77 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { OperationTypeSelect } from './columns/operation_type_select'; -import { parseCustomFieldName } from '../configurations/lens_attributes'; -import { SeriesFilter } from '../series_viewer/columns/series_filter'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) { - if (!seriesConfig) { - return null; - } - - const { selectedMetricField } = series ?? {}; - - const { hasOperationType, yAxisColumns } = seriesConfig; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( -
- - - - - - - - - - - - - {(hasOperationType || columnType === 'operation') && ( - - - - - - )} - - -
- ); -} - -const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { - defaultMessage: 'Filters', -}); - -const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { - defaultMessage: 'Operation', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx deleted file mode 100644 index 85eb85e0fc30a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; - defaultValue?: string; - metricOptions: SeriesConfig['metricOptions']; -} - -const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC'; - -export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) { - const { setSeries } = useSeriesStorage(); - - const { indexPatterns } = useAppIndexPatternContext(); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - if (!series.dataType) { - return null; - } - - const indexPattern = indexPatterns?.[series.dataType]; - - const options = (metricOptions ?? []).map(({ label, field, id }) => { - let disabled = false; - - if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { - disabled = !Boolean(indexPattern?.getFieldByName(field)); - } - return { - disabled, - value: field || id, - dropdownDisplay: disabled ? ( - {field}, - }} - /> - } - > - {label} - - ) : ( - label - ), - inputDisplay: label, - }; - }); - - return ( - onChange(value)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.selectReportMetric', - { - defaultMessage: 'Select report metric', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 8fc5ae95fd41b..eb76772a66c7e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; @@ -22,19 +22,11 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - render( - , - { - initSeries, - } - ); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 0000000000000..5d2ce6ba84951 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { FilterLabel } from '../components/filter_label'; +import { SeriesConfig, UrlFilter } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + seriesConfig: SeriesConfig; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); + + // we don't want to display report definition filters in new series view + if (isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + indexPattern={indexPattern} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + indexPattern={indexPattern} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + indexPattern={indexPattern} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 80fe400830832..c3cc8484d1751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,399 +5,134 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButtonIcon, - EuiSpacer, - EuiFormRow, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import classNames from 'classnames'; -import { isEmpty } from 'lodash'; -import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; -import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SeriesFilter } from './columns/series_filter'; +import { SeriesConfig } from '../types'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesActions } from '../series_viewer/columns/series_actions'; -import { SeriesInfo } from '../series_viewer/columns/series_info'; -import { DataTypesSelect } from './columns/data_type_select'; import { DatePickerCol } from './columns/date_picker_col'; -import { ExpandedSeriesRow } from './expanded_series_row'; -import { SeriesName } from '../series_viewer/columns/series_name'; -import { ReportTypesSelect } from './columns/report_type_select'; -import { ViewActions } from '../views/view_actions'; -import { ReportMetricOptions } from './report_metric_options'; -import { Breakdowns } from '../series_viewer/columns/breakdowns'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { SeriesActions } from './columns/series_actions'; +import { ChartEditOptions } from './chart_edit_options'; -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export interface BuilderItem { - id: number; - series: SeriesUrl; +interface EditItem { seriesConfig: SeriesConfig; + id: string; } -type ExpandedRowMap = Record; - -export const getSeriesToEdit = ({ - indexPatterns, - allSeries, - reportType, -}: { - allSeries: SeriesContextValue['allSeries']; - indexPatterns: IndexPatternState; - reportType: ReportViewType; -}): BuilderItem[] => { - const getDataViewSeries = (dataType: AppDataType) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - reportType, - indexPattern: indexPatterns[dataType], - }); - } - }; - - return allSeries.map((series, seriesIndex) => { - const seriesConfig = getDataViewSeries(series.dataType)!; - - return { id: seriesIndex, series, seriesConfig }; - }); -}; - -export const SeriesEditor = React.memo(function () { - const [editorItems, setEditorItems] = useState([]); - - const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( - {} - ); - - useEffect(() => { - const newExpandRows: ExpandedRowMap = {}; - - setEditorItems((prevState) => { - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach(({ series, id, seriesConfig }) => { - const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); - if ( - prevSeriesItem && - series.selectedMetricField && - prevSeriesItem.series.selectedMetricField !== series.selectedMetricField - ) { - newExpandRows[id] = ( - - ); - } - }); - return [...newEditorItems]; - }); - - setItemIdToExpandedRowMap((prevState) => { - return { ...prevState, ...newExpandRows }; - }); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - useEffect(() => { - setItemIdToExpandedRowMap((prevState) => { - const itemIdToExpandedRowMapValues = { ...prevState }; - - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach((item) => { - if (itemIdToExpandedRowMapValues[item.id]) { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - }); - return itemIdToExpandedRowMapValues; - }); - }, [allSeries, editorItems, indexPatterns, reportType]); - - const toggleDetails = (item: BuilderItem) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[item.id]) { - delete itemIdToExpandedRowMapValues[item.id]; - } else { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; +export function SeriesEditor() { + const { allSeries, allSeriesIds } = useSeriesStorage(); const columns = [ - { - align: 'left' as const, - width: '40px', - isExpander: true, - field: 'id', - name: '', - render: (id: number, item: BuilderItem) => - item.series.dataType && item.series.selectedMetricField ? ( - toggleDetails(item)} - isDisabled={!item.series.dataType || !item.series.selectedMetricField} - aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null, - }, - { - name: '', - field: 'id', - width: '40px', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - - ), - }, { name: i18n.translate('xpack.observability.expView.seriesEditor.name', { defaultMessage: 'Name', }), field: 'id', - width: '20%', - render: (seriesId: number, { series }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', { - defaultMessage: 'Data type', - }), - field: 'id', width: '15%', - render: (seriesId: number, { series }: BuilderItem) => ( - + render: (seriesId: string) => ( + + {' '} + {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', { - defaultMessage: 'Report metric', + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', }), - field: 'id', + field: 'defaultFilters', width: '15%', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', }), field: 'id', - width: '27%', - render: (seriesId: number, { series }: BuilderItem) => ( - + width: '25%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + ), }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - width: '10%', - field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - + name: ( +
+ +
), + width: '20%', + field: 'id', + align: 'right' as const, + render: (seriesId: string, item: EditItem) => , }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { defaultMessage: 'Actions', }), align: 'center' as const, - width: '8%', + width: '10%', field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - - ), + render: (seriesId: string, item: EditItem) => , }, ]; - const getRowProps = (item: BuilderItem) => { - const { dataType, reportDefinitions, selectedMetricField } = item.series; - - return { - className: classNames({ - isExpanded: itemIdToExpandedRowMap[item.id], - isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField, - }), - // commenting this for now, since adding on click on row, blocks adding space - // into text field for name column - // ...(dataType && selectedMetricField - // ? { - // onClick: (evt: MouseEvent) => { - // const targetElem = evt.target as HTMLElement; - // - // if ( - // targetElem.classList.contains('euiTableCellContent') && - // targetElem.tagName !== 'BUTTON' - // ) { - // toggleDetails(item); - // } - // evt.stopPropagation(); - // evt.preventDefault(); - // }, - // } - // : {}), - }; - }; - - const resetView = () => { - const totalSeries = allSeries.length; - for (let i = totalSeries; i >= 0; i--) { - removeSeries(i); - } - setEditorItems([]); - setItemIdToExpandedRowMap({}); - }; - - return ( - -
- - - - - - - - {reportType && ( - - resetView()} color="text"> - {RESET_LABEL} - - - )} - - - - - - - - {editorItems.length > 0 && ( - - )} - -
-
- ); -}); - -const Wrapper = euiStyled.div` - max-height: 50vh; - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } - - &&& { - .euiTableRow-isExpandedRow .euiTableRowCell { - border-top: none; - background-color: #FFFFFF; - border-bottom: 2px solid #d3dae6; - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - } - - .isExpanded { - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - .euiTableRowCell { - border-bottom: none; - } - } - .isIncomplete .euiTableRowCell { - background-color: rgba(254, 197, 20, 0.1); + const { indexPatterns } = useAppIndexPatternContext(); + const items: EditItem[] = []; + + allSeriesIds.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { + items.push({ + id: seriesKey, + seriesConfig: getDefaultConfigs({ + indexPattern: indexPatterns[series.dataType], + reportType: series.reportType, + dataType: series.dataType, + }), + }); } - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); - -export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { - defaultMessage: 'Reset', -}); + }); -export const REPORT_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesBuilder.reportType', - { - defaultMessage: 'Report type', + if (items.length === 0 && allSeriesIds.length > 0) { + return null; } -); - -const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', { - defaultMessage: 'Collapse', -}); -const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', { - defaultMessage: 'Exapnd', -}); + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx deleted file mode 100644 index e6ba505c82091..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx +++ /dev/null @@ -1,70 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { EuiPopover, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - useKibana, - ToolbarButton, -} from '../../../../../../../../../src/plugins/kibana_react/public'; -import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { - const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; - - const { - services: { lens }, - } = useKibana(); - - const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - return ( - setIsPopoverOpen(false)} - button={ - - id === seriesType)?.icon!} - aria-label={CHART_TYPE_LABEL} - onClick={() => setIsPopoverOpen((prevState) => !prevState)} - /> - - } - > - - - ); -} - -const EDIT_CHART_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', - { - defaultMessage: 'Edit chart type for series', - } -); - -const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { - defaultMessage: 'Chart type', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx deleted file mode 100644 index 2d38b81e12c9f..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx +++ /dev/null @@ -1,51 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: number; -} - -export function RemoveSeries({ seriesId }: Props) { - const { removeSeries, allSeries } = useSeriesStorage(); - - const onClick = () => { - removeSeries(seriesId); - }; - - const isDisabled = seriesId === 0 && allSeries.length > 1; - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx deleted file mode 100644 index 72ae111f002b1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { RemoveSeries } from './remove_series'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useDiscoverLink } from '../../hooks/use_discover_link'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SeriesActions({ seriesId, series, seriesConfig }: Props) { - const { setSeries, allSeries } = useSeriesStorage(); - - const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); - - const copySeries = () => { - let copySeriesId: string = `${series.name}-copy`; - if (allSeries.find(({ name }) => name === copySeriesId)) { - copySeriesId = copySeriesId + allSeries.length; - } - setSeries(allSeries.length, { ...series, name: copySeriesId }); - }; - - const toggleSeries = () => { - if (series.hidden) { - setSeries(seriesId, { ...series, hidden: undefined }); - } else { - setSeries(seriesId, { ...series, hidden: true }); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx deleted file mode 100644 index 87c17d03282c3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; - -interface Props { - seriesId: number; - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export interface Field { - label: string; - field: string; - nested?: string; - isNegated?: boolean; -} - -export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const options: Field[] = seriesConfig.filterFields.map((field) => { - if (typeof field === 'string') { - return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; - } - - return { - field: field.field, - nested: field.nested, - isNegated: field.isNegated, - label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], - }; - }); - - return ( - <> - {!isPreview && ( - <> - - {options.map((opt) => ( - - ))} - - - - )} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx deleted file mode 100644 index 3506acbeb528d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx +++ /dev/null @@ -1,95 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEmpty } from 'lodash'; -import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesChartTypes } from './chart_types'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesColorPicker } from '../../components/series_color_picker'; -import { dataTypes } from '../../series_editor/columns/data_type_select'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - const { dataType, reportDefinitions, selectedMetricField } = series; - - const { loading } = useAppIndexPatternContext(); - - const isIncomplete = - (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; - - if (!seriesConfig) { - return null; - } - - const { definitionFields, labels } = seriesConfig; - - const incompleteDefinition = isEmpty(reportDefinitions) - ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { - defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, - }) - : ''; - - let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; - - if (!dataType) { - incompleteMessage = MISSING_DATA_TYPE_LABEL; - } - - if (!isIncomplete && seriesConfig && isConfigure) { - return ( - - - - - - - - - ); - } - - return ( - - - {isIncomplete && {incompleteMessage}} - - {!isConfigure && ( - - - {dataTypes.find(({ id }) => id === dataType)!.label} - - - )} - - ); -} - -const MISSING_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingReportMetric', - { - defaultMessage: 'Missing report metric', - } -); - -const MISSING_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingDataType', - { - defaultMessage: 'Missing data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx deleted file mode 100644 index e35966a9fb0d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, ChangeEvent, useEffect } from 'react'; -import { EuiFieldText } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export function SeriesName({ series, seriesId }: Props) { - const { setSeries } = useSeriesStorage(); - - const [value, setValue] = useState(series.name); - - const onChange = (e: ChangeEvent) => { - setValue(e.target.value); - }; - - const onSave = () => { - if (value !== series.name) { - setSeries(seriesId, { ...series, name: value }); - } - }; - - useEffect(() => { - setValue(series.name); - }, [series.name]); - - return ; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts deleted file mode 100644 index b9ee53a7e8e2d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import dateMath from '@elastic/datemath'; -import _isString from 'lodash/isString'; - -const LAST = 'Last'; -const NEXT = 'Next'; - -const isNow = (value: string) => value === 'now'; - -export const isString = (value: any): value is string => _isString(value); -export interface QuickSelect { - timeTense: string; - timeValue: number; - timeUnits: TimeUnitId; -} -export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; -export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; - -export interface RelativeOption { - text: string; - value: TimeUnitId | TimeUnitFromNowId; -} - -export const relativeOptions: RelativeOption[] = [ - { text: 'Seconds ago', value: 's' }, - { text: 'Minutes ago', value: 'm' }, - { text: 'Hours ago', value: 'h' }, - { text: 'Days ago', value: 'd' }, - { text: 'Weeks ago', value: 'w' }, - { text: 'Months ago', value: 'M' }, - { text: 'Years ago', value: 'y' }, - - { text: 'Seconds from now', value: 's+' }, - { text: 'Minutes from now', value: 'm+' }, - { text: 'Hours from now', value: 'h+' }, - { text: 'Days from now', value: 'd+' }, - { text: 'Weeks from now', value: 'w+' }, - { text: 'Months from now', value: 'M+' }, - { text: 'Years from now', value: 'y+' }, -]; - -const timeUnitIds = relativeOptions - .map(({ value }) => value) - .filter((value) => !value.includes('+')) as TimeUnitId[]; - -export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); - -/** - * This function returns time value, time unit and time tense for a given time string. - * - * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. - * - * If given a datetime string it will return a default value. - * - * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment - * - * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. - */ -export function parseTimeParts(start: string, end: string): QuickSelect | null { - const value = isNow(start) ? end : start; - - const matches = isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/); - - if (!matches) { - return null; - } - - const operator = matches[2]; - const matchedTimeValue = matches[3]; - const timeUnits = matches[4] as TimeUnitId; - - if (matchedTimeValue && timeUnits && operator) { - return { - timeTense: operator === '+' ? NEXT : LAST, - timeUnits, - timeValue: parseInt(matchedTimeValue, 10), - }; - } - - const duration = moment.duration(moment().diff(dateMath.parse(value))); - let unitOp = ''; - for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { - const as = duration.as(relativeUnitsFromLargestToSmallest[i]); - if (as < 0) { - unitOp = '+'; - } - if (Math.abs(as) > 1) { - return { - timeValue: Math.round(Math.abs(as)), - timeUnits: relativeUnitsFromLargestToSmallest[i], - timeTense: unitOp === '+' ? NEXT : LAST, - }; - } - } - - return null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx deleted file mode 100644 index 46adba1dbde55..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; -import { useSeriesStorage } from '../hooks/use_series_storage'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { - const { setSeries } = useSeriesStorage(); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - // we don't want to display report definition filters in new series view - if (isConfigure) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId, series }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - if ((filters.length === 0 && definitionFilters.length === 0) || !indexPattern) { - return null; - } - - return ( - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - negate={false} - indexPattern={indexPattern} - /> - - )} - {(notValues ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - indexPattern={indexPattern} - /> - - )} - - ))} - - {definitionFilters.map(({ field, values }) => - values ? ( - - {}} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ) : null - )} - - {(series.filters ?? []).length > 0 && !isPreview && ( - - { - setSeries(seriesId, { ...series, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx deleted file mode 100644 index 85d65dcac6ac3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { EuiBasicTable, EuiSpacer, EuiText } from '@elastic/eui'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesInfo } from './columns/series_info'; -import { SeriesDatePicker } from '../components/series_date_picker'; -import { NO_BREAK_DOWN_LABEL } from './columns/breakdowns'; - -interface EditItem { - id: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesViewer() { - const { allSeries, reportType } = useSeriesStorage(); - - const columns = [ - { - name: '', - field: 'id', - width: '10%', - render: (seriesId: number, { seriesConfig, series }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.name', { - defaultMessage: 'Name', - }), - field: 'id', - width: '15%', - render: (seriesId: number, { series }: EditItem) => {series.name}, - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'id', - width: '35%', - render: (seriesId: number, { series, seriesConfig }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - field: 'seriesId', - width: '10%', - render: (seriesId: number, { seriesConfig: { labels }, series }: EditItem) => ( - {series.breakdown ? labels[series.breakdown] : NO_BREAK_DOWN_LABEL} - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', - }), - width: '30%', - field: 'id', - render: (seriesId: number, { series }: EditItem) => ( - - ), - }, - ]; - - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeries.forEach((series, seriesIndex) => { - if (indexPatterns[series.dataType] && !isEmpty(series.reportDefinitions)) { - items.push({ - series, - id: seriesIndex, - seriesConfig: getDefaultConfigs({ - reportType, - dataType: series.dataType, - indexPattern: indexPatterns[series.dataType], - }), - }); - } - }); - - if (items.length === 0 && allSeries.length > 0) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 4bba0c221f3c5..fbda2f4ff62e2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; +import { ExistsFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: Array; + baseFilters?: PersistableFilter[] | ExistsFilter[]; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,7 +69,6 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { - name: string; time: { to: string; from: string; @@ -77,12 +76,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; + reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - hidden?: boolean; - color?: string; + isNew?: boolean; } export interface UrlFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx deleted file mode 100644 index e0b46102caba0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx +++ /dev/null @@ -1,85 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { RefObject, useEffect, useState } from 'react'; - -import { EuiTabs, EuiTab, EuiButtonIcon } from '@elastic/eui'; -import { useHistory, useParams } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesViewer } from '../series_viewer/series_viewer'; -import { PanelId } from '../exploratory_view'; - -const tabs = [ - { - id: 'preview' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', - }), - }, - { - id: 'configure' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.configureSeries', { - defaultMessage: 'Configure series', - }), - }, -]; - -type ViewTab = 'preview' | 'configure'; - -export function SeriesViews({ - seriesBuilderRef, - onSeriesPanelCollapse, -}: { - seriesBuilderRef: RefObject; - onSeriesPanelCollapse: (panel: PanelId) => void; -}) { - const params = useParams<{ mode: ViewTab }>(); - - const history = useHistory(); - - const [selectedTabId, setSelectedTabId] = useState('configure'); - - const onSelectedTabChanged = (id: ViewTab) => { - setSelectedTabId(id); - history.push('/exploratory-view/' + id); - }; - - useEffect(() => { - setSelectedTabId(params.mode); - }, [params.mode]); - - const renderTabs = () => { - return tabs.map((tab, index) => ( - onSelectedTabChanged(tab.id)} - isSelected={tab.id === selectedTabId} - key={index} - > - {tab.id === 'preview' && selectedTabId === 'preview' ? ( - - onSeriesPanelCollapse('seriesPanel')} - /> -  {tab.name} - - ) : ( - tab.name - )} - - )); - }; - - return ( -
- {renderTabs()} - {selectedTabId === 'preview' && } - {selectedTabId === 'configure' && } -
- ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx deleted file mode 100644 index db1f23ad9b6e3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx +++ /dev/null @@ -1,119 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; -import { - allSeriesKey, - convertAllShortSeries, - NEW_SERIES_KEY, - useSeriesStorage, -} from '../hooks/use_series_storage'; -import { SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { BuilderItem, getSeriesToEdit } from '../series_editor/series_editor'; -import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; - -export function ViewActions() { - const [editorItems, setEditorItems] = useState([]); - const { - getSeries, - allSeries, - setSeries, - storage, - reportType, - autoApply, - setAutoApply, - applyChanges, - } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - const addSeries = () => { - const prevSeries = allSeries?.[0]; - const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; - const nextSeries = { name } as SeriesUrl; - - const nextSeriesId = allSeries.length; - - if (reportType === 'data-distribution') { - setSeries(nextSeriesId, { - ...nextSeries, - time: prevSeries?.time || DEFAULT_TIME, - } as SeriesUrl); - } else { - setSeries( - nextSeriesId, - prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) - ); - } - }; - - const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); - - const isAddDisabled = - !reportType || - ((reportType === ReportTypes.CORE_WEB_VITAL || - reportType === ReportTypes.DEVICE_DISTRIBUTION) && - allSeries.length > 0); - - return ( - - - setAutoApply(!autoApply)} - compressed - /> - - {!autoApply && ( - - applyChanges()} isDisabled={autoApply || noChanges} fill> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - )} - - - addSeries()} isDisabled={isAddDisabled}> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add series', - })} - - - - - ); -} - -const AUTO_APPLY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { - defaultMessage: 'Auto apply', -}); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888aa..fc562fa80e26d 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,24 +6,15 @@ */ import React, { useEffect, useState } from 'react'; -import { union, isEmpty } from 'lodash'; -import { - EuiComboBox, - EuiFormControlLayout, - EuiComboBoxOptionOption, - EuiFormRow, -} from '@elastic/eui'; +import { union } from 'lodash'; +import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set( - allowAllValuesSelection && (values ?? []).length > 0 - ? ['ALL_VALUES', ...(values ?? [])] - : values - ) + new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) ); return (uniqueValues ?? []).map((label) => ({ @@ -39,9 +30,7 @@ export function FieldValueCombobox({ loading, values, setQuery, - usePrependLabel = true, compressed = true, - required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -65,35 +54,29 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - const comboBox = ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - isInvalid={required && isEmpty(selectedValue)} - /> - ); - - return usePrependLabel ? ( + return ( - {comboBox} + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + /> - ) : ( - - {comboBox} - ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index cee3ab8aea28b..f713af9768229 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,7 +70,6 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, - allowExclusions = true, compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -174,8 +173,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} - allowExclusions={allowExclusions} isLoading={loading && !query && options.length === 0} + allowExclusions={true} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 6671c43dd8c7b..556a8e7052347 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -95,7 +95,6 @@ describe('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} - allowExclusions={true} /> ); @@ -120,7 +119,6 @@ describe('FieldValueSuggestions', () => { excludedValue={['Pak']} filters={[]} asCombobox={false} - allowExclusions={true} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 65e1d0932e4ed..54114c7604644 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,10 +28,7 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, - usePrependLabel, allowAllValuesSelection, - required, - allowExclusions = true, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -67,10 +64,7 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} - usePrependLabel={usePrependLabel} - allowExclusions={allowExclusions} allowAllValuesSelection={allowAllValuesSelection} - required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 73b3d78ce8700..d857b39b074ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,10 +23,7 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; - usePrependLabel?: boolean; - allowExclusions?: boolean; allowAllValuesSelection?: boolean; - required?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 9e7b96b02206f..01d727071770d 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,25 +18,21 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string | string[]; + value: string; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = - value instanceof Array && value.length > 1 - ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) - : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); - filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; - - filter.meta.value = value as string; + filter.meta.value = value; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; + filter.meta.type = 'phrase'; return filter; } @@ -44,10 +40,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string | string[]; + value: string; negate: boolean; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; - invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; + invertFilter: (val: { field: string; value: string; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index afc053604fcdf..9d557a40b7987 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,7 +6,6 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -27,7 +26,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - }> + ); diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 198b4092b0ed6..82a0fc39b8519 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 334733e363495..71b83b9e05324 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,7 +24,6 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; -import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -57,7 +56,6 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; - discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 09d22496c98ff..f97e3fb996441 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { alertStatusRt } from '../../common/typings'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; @@ -100,20 +99,7 @@ export const routes = { }), }, }, - '/exploratory-view/': { - handler: () => { - return ; - }, - params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), - }), - }, - }, - '/exploratory-view/:mode': { + '/exploratory-view': { handler: () => { return ; }, @@ -126,4 +112,18 @@ export const routes = { }), }, }, + // enable this to test multi series architecture + // '/exploratory-view/multi': { + // handler: () => { + // return ; + // }, + // params: { + // query: t.partial({ + // rangeFrom: t.string, + // rangeTo: t.string, + // refreshPaused: jsonRt.pipe(t.boolean), + // refreshInterval: jsonRt.pipe(t.number), + // }), + // }, + // }, }; diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts index d195198e54a73..7244066f798ba 100644 --- a/x-pack/plugins/osquery/common/types.ts +++ b/x-pack/plugins/osquery/common/types.ts @@ -9,8 +9,11 @@ import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../ export const savedQuerySavedObjectType = 'osquery-saved-query'; export const packSavedObjectType = 'osquery-pack'; -export const usageMetricSavedObjectType = 'osquery-usage-metric'; -export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack' | 'osquery-usage-metric'; +export const usageMetricSavedObjectType = 'osquery-manager-usage-metric'; +export type SavedObjectType = + | 'osquery-saved-query' + | 'osquery-pack' + | 'osquery-manager-usage-metric'; /** * This makes any optional property the same as Required would but also has the diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 75277059bbf97..083d0193be2a2 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -15,6 +15,7 @@ import { AgentIdToName } from '../agents/agent_id_to_name'; import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; +import { useActionResultsPrivileges } from './use_action_privileges'; interface ActionResultsSummaryProps { actionId: string; @@ -41,6 +42,7 @@ const ActionResultsSummaryComponent: React.FC = ({ expirationDate, ]); const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -52,6 +54,7 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); if (expired) { // @ts-expect-error update types @@ -77,6 +80,7 @@ const ActionResultsSummaryComponent: React.FC = ({ }, ], isLive, + skip: !hasActionResultsPrivileges, }); const renderAgentIdColumn = useCallback((agentId) => , []); diff --git a/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx new file mode 100644 index 0000000000000..2c80c874e89fa --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; + +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../common/lib/kibana'; +import { useErrorToast } from '../common/hooks/use_error_toast'; + +export const useActionResultsPrivileges = () => { + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); + + return useQuery( + ['actionResultsPrivileges'], + () => http.get('/internal/osquery/privileges_check'), + { + keepPreviousData: true, + select: (response) => response?.has_all_requested ?? false, + onSuccess: () => setErrorToast(), + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.action_results_privileges.fetchError', { + defaultMessage: 'Error while fetching action results privileges', + }), + }), + } + ); +}; diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index c51f2d2f44a5c..74061915d3b86 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -9,11 +9,7 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { - agentPolicyRouteService, - GetAgentPoliciesResponse, - GetAgentPoliciesResponseItem, -} from '../../../fleet/common'; +import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = () => { @@ -22,12 +18,7 @@ export const useAgentPolicies = () => { return useQuery( ['agentPolicies'], - () => - http.get(agentPolicyRouteService.getListPath(), { - query: { - perPage: 100, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/agent_policies/'), { initialData: { items: [], total: 0, page: 1, perPage: 100 }, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index dcebf136b6773..302567ef25640 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -9,7 +9,6 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; interface UseAgentPolicy { @@ -23,7 +22,7 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => { return useQuery( ['agentPolicy', { policyId }], - () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), { enabled: !skip, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 53e2ce1d53420..8a40cb171070d 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -65,9 +65,13 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh osqueryPolicyData ); const grouper = useMemo(() => new AgentGrouper(), []); - const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, { - perPage, - }); + const { isLoading: agentsLoading, data: agents } = useAllAgents( + osqueryPolicyData, + debouncedSearchValue, + { + perPage, + } + ); // option related const [options, setOptions] = useState([]); @@ -108,8 +112,8 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh grouper.setTotalAgents(totalNumAgents); grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms); grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents!); + // @ts-expect-error update types + grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents); const newOptions = grouper.generateOptions(); setOptions(newOptions); }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_details.ts b/x-pack/plugins/osquery/public/agents/use_agent_details.ts index 1a0663812dec3..b0c2fb2e1cbaf 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_details.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_details.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetOneAgentResponse, agentRouteService } from '../../../fleet/common'; +import { GetOneAgentResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -21,7 +21,7 @@ export const useAgentDetails = ({ agentId }: UseAgentDetails) => { const setErrorToast = useErrorToast(); return useQuery( ['agentDetails', agentId], - () => http.get(agentRouteService.getInfoPath(agentId)), + () => http.get(`/internal/osquery/fleet_wrapper/agents/${agentId}`), { enabled: agentId.length > 0, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts index 115b5af9d3a1b..e8d6fe7eb97ac 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts @@ -9,7 +9,7 @@ import { mapKeys } from 'lodash'; import { useQueries, UseQueryResult } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common'; +import { GetOneAgentPolicyResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = (policyIds: string[] = []) => { @@ -19,7 +19,7 @@ export const useAgentPolicies = (policyIds: string[] = []) => { const agentResponse = useQueries( policyIds.map((policyId) => ({ queryKey: ['agentPolicy', policyId], - queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + queryFn: () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), enabled: policyIds.length > 0, onSuccess: () => setErrorToast(), onError: (error) => diff --git a/x-pack/plugins/osquery/public/agents/use_agent_status.ts b/x-pack/plugins/osquery/public/agents/use_agent_status.ts index c8bc8d2fe5c0e..ba2237dbe57ea 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_status.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_status.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentStatusResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -25,7 +25,7 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => { ['agentStatus', policyId], () => http.get( - agentRouteService.getStatusPath(), + `/internal/osquery/fleet_wrapper/agent-status`, policyId ? { query: { diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index fac43eaa7ffc3..42e4954989c66 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentsResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentsResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -31,7 +31,8 @@ export const useAllAgents = ( const { perPage } = opts; const { http } = useKibana().services; const setErrorToast = useErrorToast(); - const { isLoading: agentsLoading, data: agentData } = useQuery( + + return useQuery( ['agents', osqueryPolicies, searchValue, perPage], () => { let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`; @@ -40,7 +41,7 @@ export const useAllAgents = ( kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } - return http.get(agentRouteService.getListPath(), { + return http.get(`/internal/osquery/fleet_wrapper/agents`, { query: { kuery, perPage, @@ -48,6 +49,8 @@ export const useAllAgents = ( }); }, { + // @ts-expect-error update types + select: (data) => data?.agents || [], enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0, onSuccess: () => setErrorToast(), onError: (error) => @@ -58,6 +61,4 @@ export const useAllAgents = ( }), } ); - - return { agentsLoading, agents: agentData?.list }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 9064dac1ae5d0..4b9ff931f3a91 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -10,8 +10,6 @@ import { useQuery } from 'react-query'; import { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useOsqueryPolicies = () => { @@ -20,12 +18,7 @@ export const useOsqueryPolicies = () => { const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery( ['osqueryPolicies'], - () => - http.get(packagePolicyRouteService.getListPath(), { - query: { - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/package_policies'), { select: (response) => uniq(response.items.map((p: { policy_id: string }) => p.policy_id)), diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index 236fdb1af1815..58f9f8dbec61d 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -6,11 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { find } from 'lodash/fp'; import { useQuery } from 'react-query'; -import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; import { useKibana } from '../lib/kibana'; import { useErrorToast } from './use_error_toast'; @@ -18,23 +15,12 @@ export const useOsqueryIntegration = () => { const { http } = useKibana().services; const setErrorToast = useErrorToast(); - return useQuery( - 'integrations', - () => - http.get(epmRouteService.getListPath(), { - query: { - experimental: true, - }, - }), - { - select: ({ response }: GetPackagesResponse) => - find(['name', OSQUERY_INTEGRATION_NAME], response), - onError: (error: Error) => - setErrorToast(error, { - title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { - defaultMessage: 'Error while fetching osquery integration', - }), + return useQuery('integration', () => http.get('/internal/osquery/status'), { + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { + defaultMessage: 'Error while fetching osquery integration', }), - } - ); + }), + }); }; diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx index 5be2b1816ad86..8c844d9eda3bc 100644 --- a/x-pack/plugins/osquery/public/editor/index.tsx +++ b/x-pack/plugins/osquery/public/editor/index.tsx @@ -28,13 +28,13 @@ interface OsqueryEditorProps { const OsqueryEditorComponent: React.FC = ({ defaultValue, - // disabled, + disabled, onChange, }) => ( ( + <> + + + + + + + +); + +export const DisabledCallout = React.memo(DisabledCalloutComponent); diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx index 775b5c7a06d21..67791cb34e683 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx @@ -5,16 +5,42 @@ * 2.0. */ -import React from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import React, { useEffect } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../fleet/public'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; +import { useKibana } from '../common/lib/kibana'; /** * Exports Osquery-specific package policy instructions * for use in the Fleet app custom tab */ export const OsqueryManagedCustomButtonExtension = React.memo( - () => + () => { + const [disabled, setDisabled] = React.useState(null); + const { http } = useKibana().services; + + useEffect(() => { + const fetchStatus = () => { + http.get('/internal/osquery/status').then((response) => { + setDisabled(response.install_status !== 'installed'); + }); + }; + fetchStatus(); + }, [http]); + + if (disabled === null) { + return ; + } + + return ( + <> + {disabled ? : null} + + + ); + } ); OsqueryManagedCustomButtonExtension.displayName = 'OsqueryManagedCustomButtonExtension'; diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 63036f5f693f7..9fd3c9b032ef8 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -11,7 +11,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { produce } from 'immer'; -import { i18n } from '@kbn/i18n'; import { agentRouteService, agentPolicyRouteService, @@ -29,6 +28,7 @@ import { import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table'; import { useKibana } from '../common/lib/kibana'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; import { OsqueryManagerPackagePolicy } from '../../common/types'; /** @@ -163,22 +163,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< return ( <> - {!editMode ? ( - <> - - - - - - - - ) : null} + {!editMode ? : null} {policyAgentsCount === 0 ? ( <> diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 8654a74fecfb4..bf614ff4e9bcd 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -47,6 +47,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, onSuccess, }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { http } = useKibana().services; const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const setErrorToast = useErrorToast(); @@ -175,7 +176,12 @@ const LiveQueryFormComponent: React.FC = ({ {!agentId && ( = ({ [ agentId, agentSelected, + permissions.writeSavedQueries, handleShowSaveQueryFlout, queryComponentProps, queryValueProvided, diff --git a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx index 070339bb58af2..c79fae9eb5d21 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiCodeBlock, EuiFormRow, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useRef } from 'react'; +import styled from 'styled-components'; import { OsquerySchemaLink } from '../../components/osquery_schema_link'; import { FieldHook } from '../../shared_imports'; @@ -15,6 +16,11 @@ import { SavedQueriesDropdown, SavedQueriesDropdownRef, } from '../../saved_queries/saved_queries_dropdown'; +import { useKibana } from '../../common/lib/kibana'; + +const StyledEuiCodeBlock = styled(EuiCodeBlock)` + min-height: 150px; +`; interface LiveQueryQueryFieldProps { disabled?: boolean; @@ -22,6 +28,7 @@ interface LiveQueryQueryFieldProps { } const LiveQueryQueryFieldComponent: React.FC = ({ disabled, field }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { value, setValue, errors } = field; const error = errors[0]?.message; const savedQueriesDropdownRef = useRef(null); @@ -46,12 +53,23 @@ const LiveQueryQueryFieldComponent: React.FC = ({ disa <> }> - + {!permissions.writeLiveQueries ? ( + + {value} + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 12f9025e406db..8555997d61787 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject, Subject } from 'rxjs'; import { AppMountParameters, CoreSetup, @@ -13,9 +12,6 @@ import { PluginInitializerContext, CoreStart, DEFAULT_APP_CATEGORIES, - AppStatus, - AppNavLinkStatus, - AppUpdater, } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { @@ -25,7 +21,6 @@ import { AppPluginStartDependencies, } from './types'; import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common'; -import { Installation } from '../../fleet/common'; import { LazyOsqueryManagedPolicyCreateImportExtension, LazyOsqueryManagedPolicyEditExtension, @@ -33,48 +28,7 @@ import { } from './fleet_integration'; import { getLazyOsqueryAction } from './shared_components'; -export function toggleOsqueryPlugin( - updater$: Subject, - http: CoreStart['http'], - registerExtension?: StartPlugins['fleet']['registerExtension'] -) { - if (http.anonymousPaths.isAnonymous(window.location.pathname)) { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - return; - } - - http - .fetch(`/internal/osquery/status`) - .then((response) => { - const installed = response?.install_status === 'installed'; - - if (installed && registerExtension) { - registerExtension({ - package: OSQUERY_INTEGRATION_NAME, - view: 'package-detail-custom', - Component: LazyOsqueryManagedCustomButtonExtension, - }); - } - - updater$.next(() => ({ - navLinkStatus: installed ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, - })); - }) - .catch(() => { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - }); -} - export class OsqueryPlugin implements Plugin { - private readonly appUpdater$ = new BehaviorSubject(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, - })); private kibanaVersion: string; private storage = new Storage(localStorage); @@ -102,8 +56,6 @@ export class OsqueryPlugin implements Plugin ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); + + registerExtension({ + package: OSQUERY_INTEGRATION_NAME, + view: 'package-detail-custom', + Component: LazyOsqueryManagedCustomButtonExtension, + }); } return { diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index d82737ab51e7c..d293847215d68 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -8,6 +8,7 @@ import { isEmpty, isEqual, keys, map } from 'lodash/fp'; import { EuiCallOut, + EuiCode, EuiDataGrid, EuiDataGridSorting, EuiDataGridProps, @@ -31,6 +32,8 @@ import { ViewResultsInLensAction, ViewResultsActionButtonType, } from '../scheduled_query_groups/scheduled_query_group_queries_table'; +import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; +import { OSQUERY_INTEGRATION_NAME } from '../../common'; const DataContext = createContext([]); @@ -49,6 +52,7 @@ const ResultsTableComponent: React.FC = ({ endDate, }) => { const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations }, @@ -60,6 +64,7 @@ const ResultsTableComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); const { getUrlForApp } = useKibana().services.application; @@ -104,6 +109,7 @@ const ResultsTableComponent: React.FC = ({ field: sortedColumn.id, direction: sortedColumn.direction as Direction, })), + skip: !hasActionResultsPrivileges, }); const [visibleColumns, setVisibleColumns] = useState([]); @@ -237,6 +243,17 @@ const ResultsTableComponent: React.FC = ({ ] ); + if (!hasActionResultsPrivileges) { + return ( + +

+ You're missing read privileges to read from + logs-{OSQUERY_INTEGRATION_NAME}.result*. +

+
+ ); + } + if (!isFetched) { return ; } diff --git a/x-pack/plugins/osquery/public/routes/components/index.ts b/x-pack/plugins/osquery/public/routes/components/index.ts new file mode 100644 index 0000000000000..877c25fe7cdad --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './missing_privileges'; diff --git a/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx b/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx new file mode 100644 index 0000000000000..6adabff599124 --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; + +const Panel = styled(EuiPanel)` + max-width: 500px; + margin-right: auto; + margin-left: auto; +`; + +const MissingPrivilegesComponent = () => ( +
+ + + + +

+ } + body={ +

+ +

+ } + /> + + + +); + +export const MissingPrivileges = React.memo(MissingPrivilegesComponent); diff --git a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx index af039e85e9785..47815516dd726 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx @@ -12,15 +12,26 @@ import { LiveQueriesPage } from './list'; import { NewLiveQueryPage } from './new'; import { LiveQueryDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const LiveQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const match = useRouteMatch(); + if (!permissions.readLiveQueries) { + return ; + } + return ( - + {permissions.runSavedQueries || permissions.writeLiveQueries ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx index 90ac7b5cc17ae..23bc44b455405 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx @@ -9,13 +9,14 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { ActionsTable } from '../../../actions/actions_table'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const LiveQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const newQueryLinkProps = useRouterNavigate('live_queries/new'); @@ -40,14 +41,19 @@ const LiveQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeLiveQueries, permissions.runSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx index 8d77b7819bd3e..a7596575b90c4 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx @@ -24,11 +24,13 @@ import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_f interface EditSavedQueryFormProps { defaultValue?: unknown; handleSubmit: () => Promise; + viewMode?: boolean; } const EditSavedQueryFormComponent: React.FC = ({ defaultValue, handleSubmit, + viewMode, }) => { const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -39,41 +41,45 @@ const EditSavedQueryFormComponent: React.FC = ({ return (
- - - - - + + {!viewMode && ( + <> + + - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + )} ); }; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 5bdba133fad72..401966460a7e7 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -17,7 +17,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { useParams } from 'react-router-dom'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; @@ -25,6 +25,8 @@ import { EditSavedQueryForm } from './form'; import { useDeleteSavedQuery, useUpdateSavedQuery, useSavedQuery } from '../../../saved_queries'; const EditSavedQueryPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const { savedQueryId } = useParams<{ savedQueryId: string }>(); const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -35,6 +37,8 @@ const EditSavedQueryPageComponent = () => { useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); + const viewMode = useMemo(() => !permissions.writeSavedQueries, [permissions.writeSavedQueries]); + const handleCloseDeleteConfirmationModal = useCallback(() => { setIsDeleteModalVisible(false); }, []); @@ -63,21 +67,32 @@ const EditSavedQueryPageComponent = () => {

- + {viewMode ? ( + + ) : ( + + )}

), - [savedQueryDetails?.attributes?.id, savedQueryListProps] + [savedQueryDetails?.attributes?.id, savedQueryListProps, viewMode] ); const RightColumn = useMemo( @@ -95,12 +110,17 @@ const EditSavedQueryPageComponent = () => { if (isLoading) return null; return ( - + {!isLoading && !isEmpty(savedQueryDetails) && ( )} {isDeleteModalVisible ? ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx index f986129bdfefc..a2241ae017df4 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx @@ -12,15 +12,22 @@ import { QueriesPage } from './list'; import { NewSavedQueryPage } from './new'; import { EditSavedQueryPage } from './edit'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { MissingPrivileges } from '../components'; +import { useKibana } from '../../common/lib/kibana'; const SavedQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('saved_queries'); const match = useRouteMatch(); + if (!permissions.readSavedQueries) { + return ; + } + return ( - + {permissions.writeSavedQueries ? : } diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index 0c04e816dae7a..e82dcf85780e1 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -21,16 +21,63 @@ import { useHistory } from 'react-router-dom'; import { SavedObject } from 'kibana/public'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; import { useSavedQueries } from '../../../saved_queries/use_saved_queries'; +interface PlayButtonProps { + disabled: boolean; + savedQueryId: string; + savedQueryName: string; +} + +const PlayButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { + const { push } = useHistory(); + + // TODO: Fix href + const handlePlayClick = useCallback( + () => + push('/live_queries/new', { + form: { + savedQueryId, + }, + }), + [push, savedQueryId] + ); + + return ( + + ); +}; + +const PlayButton = React.memo(PlayButtonComponent); + interface EditButtonProps { + disabled?: boolean; savedQueryId: string; savedQueryName: string; } -const EditButtonComponent: React.FC = ({ savedQueryId, savedQueryName }) => { +const EditButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`); return ( @@ -38,6 +85,7 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue color="primary" {...buttonProps} iconType="pencil" + isDisabled={disabled} aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', { defaultMessage: 'Edit {savedQueryName}', values: { @@ -51,8 +99,9 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue const EditButton = React.memo(EditButtonComponent); const SavedQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + useBreadcrumbs('saved_queries'); - const { push } = useHistory(); const newQueryLinkProps = useRouterNavigate('saved_queries/new'); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(20); @@ -61,16 +110,6 @@ const SavedQueriesPageComponent = () => { const { data } = useSavedQueries({ isLive: true }); - const handlePlayClick = useCallback( - (item) => - push('/live_queries/new', { - form: { - savedQueryId: item.id, - }, - }), - [push] - ); - const renderEditAction = useCallback( (item: SavedObject<{ name: string }>) => ( @@ -78,6 +117,17 @@ const SavedQueriesPageComponent = () => { [] ); + const renderPlayAction = useCallback( + (item: SavedObject<{ name: string }>) => ( + + ), + [permissions.runSavedQueries, permissions.writeLiveQueries] + ); + const renderUpdatedAt = useCallback((updatedAt, item) => { if (!updatedAt) return '-'; @@ -128,17 +178,10 @@ const SavedQueriesPageComponent = () => { name: i18n.translate('xpack.osquery.savedQueries.table.actionsColumnTitle', { defaultMessage: 'Actions', }), - actions: [ - { - type: 'icon', - icon: 'play', - onClick: handlePlayClick, - }, - { render: renderEditAction }, - ], + actions: [{ render: renderPlayAction }, { render: renderEditAction }], }, ], - [handlePlayClick, renderEditAction, renderUpdatedAt] + [renderEditAction, renderPlayAction, renderUpdatedAt] ); const onTableChange = useCallback(({ page = {}, sort = {} }) => { @@ -189,14 +232,19 @@ const SavedQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx index 960de043eac6e..dc6df49615093 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx @@ -21,7 +21,7 @@ import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group'; import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table'; @@ -36,6 +36,7 @@ const Divider = styled.div` `; const ScheduledQueryGroupDetailsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>(); const scheduledQueryGroupsListProps = useRouterNavigate('scheduled_query_groups'); const editQueryLinkProps = useRouterNavigate( @@ -111,7 +112,12 @@ const ScheduledQueryGroupDetailsPageComponent = () => { - + { ), - [data?.policy_id, editQueryLinkProps] + [data?.policy_id, editQueryLinkProps, permissions] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx index 76ca2bf14d303..53bf4ae79a908 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx @@ -13,18 +13,25 @@ import { AddScheduledQueryGroupPage } from './add'; import { EditScheduledQueryGroupPage } from './edit'; import { ScheduledQueryGroupDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const ScheduledQueryGroupsComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('scheduled_query_groups'); const match = useRouteMatch(); + if (!permissions.readPacks) { + return ; + } + return ( - + {permissions.writePacks ? : } - + {permissions.writePacks ? : } diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx index b02ef95498b5c..006dd0e6ec1b6 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx @@ -9,12 +9,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { ScheduledQueryGroupsTable } from '../../../scheduled_query_groups/scheduled_query_groups_table'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const ScheduledQueryGroupsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const newQueryLinkProps = useRouterNavigate('scheduled_query_groups/add'); const LeftColumn = useMemo( @@ -38,14 +39,19 @@ const ScheduledQueryGroupsPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [newQueryLinkProps, permissions.writePacks] ); return ( diff --git a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx index 9bbf847c4d2a0..beff34a8919a0 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,64 +17,78 @@ import { CodeEditorField } from './code_editor_field'; export const CommonUseField = getUseField({ component: Field }); -const SavedQueryFormComponent = () => ( - <> - - - - - - - - - -
+interface SavedQueryFormProps { + viewMode?: boolean; +} + +const SavedQueryFormComponent: React.FC = ({ viewMode }) => { + const euiFieldProps = useMemo( + () => ({ + isDisabled: !!viewMode, + }), + [viewMode] + ); + + return ( + <> + + + + + + + + + +
+ +
+
+ -
-
- - +
+
+ + + + + + -
- - - - - - - - - - - - - - - -); + + + + + + + + ); +}; export const SavedQueryForm = React.memo(SavedQueryFormComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx index bcb47d0adc833..7f26534626b12 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx @@ -28,12 +28,16 @@ const StyledEuiLoadingSpinner = styled(EuiLoadingSpinner)` `; interface ActiveStateSwitchProps { + disabled?: boolean; item: PackagePolicy; } const ActiveStateSwitchComponent: React.FC = ({ item }) => { const queryClient = useQueryClient(); const { + application: { + capabilities: { osquery: permissions }, + }, http, notifications: { toasts }, } = useKibana().services; @@ -126,7 +130,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) {isLoading && } ( ['scheduledQueryGroup', { scheduledQueryGroupId }], - () => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)), + () => http.get(`/internal/osquery/scheduled_query_group/${scheduledQueryGroupId}`), { keepPreviousData: true, enabled: !skip || !scheduledQueryGroupId, diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts index 3302d8e621eb7..01b67a3d5164a 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts @@ -9,12 +9,7 @@ import { produce } from 'immer'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { - ListResult, - PackagePolicy, - packagePolicyRouteService, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '../../../fleet/common'; +import { ListResult, PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; export const useScheduledQueryGroups = () => { @@ -23,7 +18,7 @@ export const useScheduledQueryGroups = () => { return useQuery>( ['scheduledQueries'], () => - http.get(packagePolicyRouteService.getListPath(), { + http.get('/internal/osquery/scheduled_query_group', { query: { page: 1, perPage: 10000, diff --git a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts index 6ebf469b8fb29..ca4fd1ebeffd2 100644 --- a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts +++ b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, LoggerFactory } from 'src/core/server'; +import { CoreSetup, Logger, LoggerFactory } from '../../../../../src/core/server'; import { SecurityPluginStart } from '../../../security/server'; import { AgentService, @@ -71,6 +71,7 @@ export interface OsqueryAppContext { logFactory: LoggerFactory; config(): ConfigType; security: SecurityPluginStart; + getStartServices: CoreSetup['getStartServices']; /** * Object readiness is tied to plugin start method */ diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 6bc12f5736e5e..ff8483fdb385a 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,14 +5,20 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, +} from '../../fleet/common'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, + DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; - import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; @@ -21,6 +27,169 @@ import { initSavedObjects } from './saved_objects'; import { initUsageCollectors } from './usage'; import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_context_services'; import { ConfigType } from './config'; +import { packSavedObjectType, savedQuerySavedObjectType } from '../common/types'; +import { PLUGIN_ID } from '../common'; + +const registerFeatures = (features: SetupPlugins['features']) => { + features.registerKibanaFeature({ + id: PLUGIN_ID, + name: i18n.translate('xpack.osquery.features.osqueryFeatureName', { + defaultMessage: 'Osquery', + }), + category: DEFAULT_APP_CATEGORIES.management, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + order: 2300, + excludeFromBasePrivileges: true, + privileges: { + all: { + api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-write`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + read: [PACKAGES_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE], + }, + ui: ['write'], + }, + read: { + api: [`${PLUGIN_ID}-read`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [], + read: [ + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + ], + }, + ui: ['read'], + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.osquery.features.liveQueriesSubFeatureName', { + defaultMessage: 'Live queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writeLiveQueries`, `${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeLiveQueries', 'readLiveQueries'], + }, + { + api: [`${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readLiveQueries'], + }, + ], + }, + { + groupType: 'independent', + privileges: [ + { + api: [`${PLUGIN_ID}-runSavedQueries`], + id: 'run_saved_queries', + name: i18n.translate('xpack.osquery.features.runSavedQueriesPrivilegeName', { + defaultMessage: 'Run Saved queries', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: ['runSavedQueries'], + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.osquery.features.savedQueriesSubFeatureName', { + defaultMessage: 'Saved queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'saved_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [savedQuerySavedObjectType], + read: [], + }, + ui: ['writeSavedQueries', 'readSavedQueries'], + }, + { + id: 'saved_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [savedQuerySavedObjectType], + }, + ui: ['readSavedQueries'], + }, + ], + }, + ], + }, + { + // TODO: Rename it to "Packs" as part of https://github.com/elastic/kibana/pull/107345 + name: i18n.translate('xpack.osquery.features.scheduledQueryGroupsSubFeatureName', { + defaultMessage: 'Scheduled query groups', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writePacks`], + id: 'packs_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [packSavedObjectType], + read: [], + }, + ui: ['writePacks', 'readPacks'], + }, + { + api: [`${PLUGIN_ID}-readPacks`], + id: 'packs_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [packSavedObjectType], + }, + ui: ['readPacks'], + }, + ], + }, + ], + }, + ], + }); +}; export class OsqueryPlugin implements Plugin { private readonly logger: Logger; @@ -40,10 +209,13 @@ export class OsqueryPlugin implements Plugin config, security: plugins.security, diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 478bfc1053bdf..79c1149675b0d 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import moment from 'moment'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -19,6 +20,7 @@ import { } from '../../../common/schemas/routes/action/create_action_request_body_schema'; import { incrementCount } from '../usage'; +import { getInternalSavedObjectsClient } from '../../usage/collector'; export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( @@ -30,10 +32,17 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon CreateActionRequestBodySchema >(createActionRequestBodySchema), }, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`, `access:${PLUGIN_ID}-runSavedQueries`], + }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + const esClient = context.core.elasticsearch.client.asInternalUser; const soClient = context.core.savedObjects.client; + const internalSavedObjectsClient = await getInternalSavedObjectsClient( + osqueryContext.getStartServices + ); + const { agentSelection } = request.body as { agentSelection: AgentSelection }; const selectedAgents = await parseAgentSelection( esClient, @@ -41,12 +50,14 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon osqueryContext, agentSelection ); - incrementCount(soClient, 'live_query'); + incrementCount(internalSavedObjectsClient, 'live_query'); if (!selectedAgents.length) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.badRequest({ body: new Error('No agents found for selection') }); } + // TODO: Add check for `runSavedQueries` only + try { const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username; const action = { @@ -74,7 +85,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon }, }); } catch (error) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.customError({ statusCode: 500, body: new Error(`Error occurred while processing ${error}`), diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts new file mode 100644 index 0000000000000..67b4a27ab9ec7 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents/{id}', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agent = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.getAgent(esClient, request.params.id); + + return response.ok({ body: { item: agent } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts new file mode 100644 index 0000000000000..e35e776cb1958 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const agentPolicies = await osqueryContext.service.getAgentPolicyService()?.list(soClient, { + ...(request.query || {}), + perPage: 100, + }); + + return response.ok({ body: agentPolicies }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts new file mode 100644 index 0000000000000..f845b04e99c93 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const packageInfo = await osqueryContext.service + .getAgentPolicyService() + ?.get(soClient, request.params.id); + + return response.ok({ body: { item: packageInfo } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts new file mode 100644 index 0000000000000..dea4402472958 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { GetAgentStatusResponse } from '../../../../fleet/common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentStatusForAgentPolicyRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent-status', + validate: { + query: schema.object({ + policyId: schema.string(), + kuery: schema.maybe(schema.string()), + }), + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const results = await osqueryContext.service + .getAgentService() + ?.getAgentStatusForAgentPolicy(esClient, request.query.policyId, request.query.kuery); + + if (!results) { + return response.ok({ body: {} }); + } + + const body: GetAgentStatusResponse = { results }; + + return response.ok({ body }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts new file mode 100644 index 0000000000000..d45cb26e0d199 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agents = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.listAgents(esClient, request.query); + + return response.ok({ body: agents }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts similarity index 81% rename from x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts index 43d5f3fc893f0..b95dfbdfb9cb4 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts @@ -6,19 +6,19 @@ */ import { schema } from '@kbn/config-schema'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; - +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const findScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { - path: '/internal/osquery/scheduled_query', + path: '/internal/osquery/fleet_wrapper/package_policies', validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts new file mode 100644 index 0000000000000..1821e19da975e --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import { getAgentPoliciesRoute } from './get_agent_policies'; +import { getAgentPolicyRoute } from './get_agent_policy'; +import { getAgentStatusForAgentPolicyRoute } from './get_agent_status_for_agent_policy'; +import { getPackagePoliciesRoute } from './get_package_policies'; +import { getAgentsRoute } from './get_agents'; +import { getAgentDetailsRoute } from './get_agent_details'; + +export const initFleetWrapperRoutes = (router: IRouter, context: OsqueryAppContext) => { + getAgentDetailsRoute(router, context); + getAgentPoliciesRoute(router, context); + getAgentPolicyRoute(router, context); + getAgentStatusForAgentPolicyRoute(router, context); + getPackagePoliciesRoute(router, context); + getAgentsRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index dd11141b2553f..c927c711a23cb 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -10,13 +10,19 @@ import { initActionRoutes } from './action'; import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initSavedQueryRoutes } from './saved_query'; import { initStatusRoutes } from './status'; +import { initFleetWrapperRoutes } from './fleet_wrapper'; import { initPackRoutes } from './pack'; +import { initScheduledQueryGroupRoutes } from './scheduled_query_group'; +import { initPrivilegesCheckRoutes } from './privileges_check'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { const config = context.config(); initActionRoutes(router, context); initStatusRoutes(router, context); + initScheduledQueryGroupRoutes(router, context); + initFleetWrapperRoutes(router, context); + initPrivilegesCheckRoutes(router, context); if (config.packs) { initPackRoutes(router); diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/index.ts b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts new file mode 100644 index 0000000000000..8932b23b85f5a --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { privilegesCheckRoute } from './privileges_check_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const initPrivilegesCheckRoutes = (router: IRouter, context: OsqueryAppContext) => { + privilegesCheckRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts new file mode 100644 index 0000000000000..80c335c1c46d3 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/privileges_check', + validate: {}, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`], + }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asCurrentUser; + + const privileges = ( + await esClient.security.hasPrivileges({ + body: { + index: [ + { + names: [`logs-${OSQUERY_INTEGRATION_NAME}.result*`], + privileges: ['read'], + }, + ], + }, + }) + ).body; + + return response.ok({ + body: privileges, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index a41cb7cc39b40..fe8220c559de8 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { IRouter } from '../../../../../../src/core/server'; - +import { PLUGIN_ID } from '../../../common'; import { createSavedQueryRequestSchema, CreateSavedQueryRequestSchemaDecoded, @@ -24,6 +24,7 @@ export const createSavedQueryRoute = (router: IRouter) => { CreateSavedQueryRequestSchemaDecoded >(createSavedQueryRequestSchema), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts index 5b8e231ba61ec..a34db8c11ddc3 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const deleteSavedQueryRoute = (router: IRouter) => { validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts index 6d737ba0d0220..79d6927d06722 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const findSavedQueryRoute = (router: IRouter) => { validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts index 2d399648df4cc..4157ed1582305 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const readSavedQueryRoute = (router: IRouter) => { validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index f9ecf675489dc..8edf95e311543 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts index a3b882392989f..831fb30f6e320 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts @@ -6,16 +6,18 @@ */ import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; export const createScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { - path: '/internal/osquery/scheduled', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asCurrentUser; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts index 5b8e231ba61ec..c914512bb155e 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts @@ -6,17 +6,18 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; export const deleteSavedQueryRoute = (router: IRouter) => { router.delete( { - path: '/internal/osquery/saved_query', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts new file mode 100644 index 0000000000000..15c45e09b1bfd --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const findScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/scheduled_query_group', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, + }, + async (context, request, response) => { + const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; + const packagePolicyService = osqueryContext.service.getPackagePolicyService(); + const policies = await packagePolicyService?.list(context.core.savedObjects.client, { + kuery, + }); + + return response.ok({ + body: policies, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts similarity index 67% rename from x-pack/plugins/osquery/server/routes/scheduled_query/index.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts index 706bc38397296..416981a5cb5f2 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts @@ -10,14 +10,14 @@ import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; // import { createScheduledQueryRoute } from './create_scheduled_query_route'; // import { deleteScheduledQueryRoute } from './delete_scheduled_query_route'; -import { findScheduledQueryRoute } from './find_scheduled_query_route'; -import { readScheduledQueryRoute } from './read_scheduled_query_route'; +import { findScheduledQueryGroupRoute } from './find_scheduled_query_group_route'; +import { readScheduledQueryGroupRoute } from './read_scheduled_query_group_route'; // import { updateScheduledQueryRoute } from './update_scheduled_query_route'; -export const initScheduledQueryRoutes = (router: IRouter, context: OsqueryAppContext) => { +export const initScheduledQueryGroupRoutes = (router: IRouter, context: OsqueryAppContext) => { // createScheduledQueryRoute(router); // deleteScheduledQueryRoute(router); - findScheduledQueryRoute(router, context); - readScheduledQueryRoute(router, context); + findScheduledQueryGroupRoute(router, context); + readScheduledQueryGroupRoute(router, context); // updateScheduledQueryRoute(router); }; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts similarity index 65% rename from x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts index 009374f6a2e9e..de8125aab5b29 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts @@ -6,28 +6,34 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const readScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const readScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { router.get( { - path: '/internal/osquery/scheduled_query/{id}', + path: '/internal/osquery/scheduled_query_group/{id}', validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const packagePolicyService = osqueryContext.service.getPackagePolicyService(); - // @ts-expect-error update types - const scheduledQuery = await packagePolicyService?.get(savedObjectsClient, request.params.id); + const scheduledQueryGroup = await packagePolicyService?.get( + savedObjectsClient, + // @ts-expect-error update types + request.params.id + ); return response.ok({ - // @ts-expect-error update types - body: scheduledQuery, + body: { item: scheduledQueryGroup }, }); } ); diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts similarity index 92% rename from x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts index efb4f2990e942..2a6e7a33fcddd 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts index d7ea49c6152cd..0a527424f9f42 100644 --- a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts +++ b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -14,16 +14,10 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon { path: '/internal/osquery/status', validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const soClient = context.core.savedObjects.client; - const isSuperUser = osqueryContext.security.authc - .getCurrentUser(request) - ?.roles.includes('superuser'); - - if (!isSuperUser) { - return response.ok({ body: undefined }); - } const packageInfo = await osqueryContext.service .getPackageService() diff --git a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts index 92709f92d9e5f..603bcad87cf80 100644 --- a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts @@ -23,6 +23,6 @@ export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = { export const usageMetricType: SavedObjectsType = { name: usageMetricSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'agnostic', mappings: usageMetricSavedObjectMappings, }; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts index 9fffb0726dce6..2fa9ee04ab534 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts @@ -23,7 +23,7 @@ import { OsqueryFactory } from './factory/types'; export const osquerySearchStrategyProvider = ( data: PluginStart ): ISearchStrategy, StrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + let es: typeof data.search.searchAsInternalUser; return { search: (request, options, deps) => { @@ -32,20 +32,35 @@ export const osquerySearchStrategyProvider = ( } const queryFactory: OsqueryFactory = osqueryFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - return es.search({ ...request, params: dsl }, options, deps).pipe( - map((response) => { - return { - ...response, - ...{ - rawResponse: shimHitsTotal(response.rawResponse), - }, - }; - }), - mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) - ); + + // use internal user for searching .fleet* indicies + es = dsl.index?.includes('fleet') + ? data.search.searchAsInternalUser + : data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return es + .search( + { + ...request, + params: dsl, + }, + options, + deps + ) + .pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); }, cancel: async (id, options, deps) => { - if (es.cancel) { + if (es?.cancel) { return es.cancel(id, options, deps); } }, diff --git a/x-pack/plugins/osquery/server/usage/collector.ts b/x-pack/plugins/osquery/server/usage/collector.ts index 4432592a4e063..b04fc34e52453 100644 --- a/x-pack/plugins/osquery/server/usage/collector.ts +++ b/x-pack/plugins/osquery/server/usage/collector.ts @@ -11,11 +11,12 @@ import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers import { CollectorDependencies, usageSchema, UsageData } from './types'; export type RegisterCollector = (deps: CollectorDependencies) => void; -export async function getInternalSavedObjectsClient(core: CoreSetup) { - return core.getStartServices().then(async ([coreStart]) => { - return coreStart.savedObjects.createInternalRepository(); - }); -} +export const getInternalSavedObjectsClient = async ( + getStartServices: CoreSetup['getStartServices'] +) => { + const [coreStart] = await getStartServices(); + return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); +}; export const registerCollector: RegisterCollector = ({ core, osqueryContext, usageCollection }) => { if (!usageCollection) { @@ -26,7 +27,8 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa schema: usageSchema, isReady: () => true, fetch: async ({ esClient }: CollectorFetchContext): Promise => { - const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core)); + const savedObjectsClient = await getInternalSavedObjectsClient(core.getStartServices); + return { beat_metrics: { usage: await getBeatUsage(esClient), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1b20d91d480b2..c8e3526005963 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1523,9 +1523,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "インデックスが存在することを確認してください。", "discover.docTable.limitedSearchResultLabel": "{resultCount}件の結果のみが表示されます。検索結果を絞り込みます。", "discover.docTable.noResultsTitle": "結果が見つかりませんでした", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表内の次ページ", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表内の前ページ", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "ドキュメント", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "{columnName}列を左に移動", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "列を左に移動", @@ -18517,10 +18514,20 @@ "xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル", "xpack.observability.expView.operationType.average": "平均", "xpack.observability.expView.operationType.median": "中央", + "xpack.observability.expView.reportType.noDataType": "データ型を選択すると、系列の構築を開始します。", + "xpack.observability.expView.seriesBuilder.breakdown": "内訳", + "xpack.observability.expView.seriesBuilder.dataType": "データ型", + "xpack.observability.expView.seriesBuilder.definition": "定義", + "xpack.observability.expView.seriesBuilder.filters": "フィルター", + "xpack.observability.expView.seriesBuilder.report": "レポート", "xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプを選択すると、ビジュアライゼーションを定義します。", + "xpack.observability.expView.seriesEditor.actions": "アクション", + "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します", + "xpack.observability.expView.seriesEditor.breakdowns": "内訳", "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", "xpack.observability.expView.seriesEditor.filters": "フィルター", "xpack.observability.expView.seriesEditor.name": "名前", + "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", "xpack.observability.featureCatalogueDescription1": "インフラストラクチャメトリックを監視します。", @@ -18586,6 +18593,7 @@ "xpack.observability.overview.uptime.up": "アップ", "xpack.observability.overview.ux.appLink": "アプリで表示", "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", + "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.title": "リソース", @@ -18599,6 +18607,7 @@ "xpack.observability.section.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", + "xpack.observability.seriesEditor.edit": "系列を編集", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均", "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3c89b74eca8b..cf31ec8d8eceb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1532,9 +1532,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "请确保索引存在。", "discover.docTable.limitedSearchResultLabel": "仅限于 {resultCount} 个结果。优化您的搜索。", "discover.docTable.noResultsTitle": "找不到结果", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表中下一页", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表中上一页", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "文档", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "向左移动“{columnName}”列", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "向左移动列", @@ -18933,10 +18930,20 @@ "xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位", "xpack.observability.expView.operationType.average": "平均值", "xpack.observability.expView.operationType.median": "中值", + "xpack.observability.expView.reportType.noDataType": "选择数据类型以开始构建序列。", + "xpack.observability.expView.seriesBuilder.breakdown": "分解", + "xpack.observability.expView.seriesBuilder.dataType": "数据类型", + "xpack.observability.expView.seriesBuilder.definition": "定义", + "xpack.observability.expView.seriesBuilder.filters": "筛选", + "xpack.observability.expView.seriesBuilder.report": "报告", "xpack.observability.expView.seriesBuilder.selectReportType": "选择报告类型以定义可视化。", + "xpack.observability.expView.seriesEditor.actions": "操作", + "xpack.observability.expView.seriesEditor.addFilter": "添加筛选", + "xpack.observability.expView.seriesEditor.breakdowns": "分解", "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", "xpack.observability.expView.seriesEditor.filters": "筛选", "xpack.observability.expView.seriesEditor.name": "名称", + "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", "xpack.observability.featureCatalogueDescription1": "监测基础架构指标。", @@ -19002,6 +19009,7 @@ "xpack.observability.overview.uptime.up": "运行", "xpack.observability.overview.ux.appLink": "在应用中查看", "xpack.observability.overview.ux.title": "用户体验", + "xpack.observability.reportTypeCol.nodata": "没有可用数据", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.title": "资源", @@ -19015,6 +19023,7 @@ "xpack.observability.section.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", + "xpack.observability.seriesEditor.edit": "编辑序列", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均值", "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index aa981071b7ee2..1a53a2c9b64a0 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -22,7 +22,6 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import numeral from '@elastic/numeral'; import moment from 'moment'; -import { useSelector } from 'react-redux'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; import { UptimeThemeContext } from '../../../contexts'; @@ -33,7 +32,6 @@ import { getDateRangeFromChartElement } from './utils'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; -import { monitorStatusSelector } from '../../../state/selectors'; export interface PingHistogramComponentProps { /** @@ -75,8 +73,6 @@ export const PingHistogramComponent: React.FC = ({ const monitorId = useMonitorId(); - const selectedMonitor = useSelector(monitorStatusSelector); - const { basePath } = useUptimeSettingsContext(); const [getUrlParams, updateUrlParams] = useUrlParams(); @@ -193,21 +189,12 @@ export const PingHistogramComponent: React.FC = ({ const pingHistogramExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-pings`, - dataType: 'synthetics', - selectedMetricField: 'summary.up', - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.name': - monitorId && selectedMonitor?.monitor?.name - ? [selectedMonitor.monitor.name] - : ['ALL_VALUES'], - }, - }, - ], + 'pings-over-time': { + dataType: 'synthetics', + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), + }, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index c459fe46da975..9f00dd2e8f061 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -10,15 +10,13 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { createExploratoryViewUrl } from '../../../../../observability/public'; +import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; -import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { defaultMessage: 'Add data', @@ -40,28 +38,13 @@ export function ActionMenuContent(): React.ReactElement { const { dateRangeStart, dateRangeEnd } = params; const history = useHistory(); - const selectedMonitor = useSelector(monitorStatusSelector); - - const monitorId = selectedMonitor?.monitor?.id; - const syntheticExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'synthetics', - seriesType: 'area_stacked', - selectedMetricField: 'monitor.duration.us', - time: { from: dateRangeStart, to: dateRangeEnd }, - breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', - reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], - }, - name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', - }, - ], + 'synthetics-series': ({ + dataType: 'synthetics', + isNew: true, + time: { from: dateRangeStart, to: dateRangeEnd }, + } as unknown) as SeriesUrl, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 18aba948eaa37..1590e225f9ca8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -55,19 +55,16 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-response-duration`, - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], - }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', + [`monitor-duration`]: { + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.id': [monitorId] as string[], }, - ], + breakdown: 'observer.geo.name', + operationType: 'average', + dataType: 'synthetics', + }, }, basePath ); diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 275626664bef0..6a6a0e13a1e1e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -115,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) { 'logs', 'maps', 'observabilityCases', + 'osquery', 'uptime', 'siem', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 2576a5eaf9bc9..bbb0fc60cb3ce 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -70,6 +70,19 @@ export default function ({ getService }: FtrProviderContext) { indexPatterns: ['all', 'read'], savedObjectsManagement: ['all', 'read'], timelion: ['all', 'read'], + osquery: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'live_queries_all', + 'live_queries_read', + 'run_saved_queries', + 'saved_queries_all', + 'saved_queries_read', + 'packs_all', + 'packs_read', + ], }, reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 25266da2cdfb3..dc00be028412b 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], + osquery: ['all', 'read'], ml: ['all', 'read'], siem: ['all', 'read'], fleet: ['all', 'read'], diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index 073d08c5b1d8d..8c6603a3e38b0 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -14,6 +14,8 @@ const TEST_INDEX = 'logs-log.log-test'; const FINAL_PIPELINE_ID = '.fleet_final_pipeline-1'; +const FINAL_PIPELINE_VERSION = 1; + let pkgKey: string; export default function (providerContext: FtrProviderContext) { @@ -81,11 +83,32 @@ export default function (providerContext: FtrProviderContext) { } }); + it('should correctly update the final pipeline', async () => { + await es.ingest.putPipeline({ + id: FINAL_PIPELINE_ID, + body: { + description: 'Test PIPELINE WITHOUT version', + processors: [ + { + set: { + field: 'my-keyword-field', + value: 'foo', + }, + }, + ], + }, + }); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx'); + const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); + expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); + expect(pipelineRes.body[FINAL_PIPELINE_ID].version).to.be(1); + }); + it('should correctly setup the final pipeline and apply to fleet managed index template', async () => { const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); const res = await es.indices.getIndexTemplate({ name: 'logs-log.log' }); - expect(res.body.index_templates.length).to.be(1); + expect(res.body.index_templates.length).to.be(FINAL_PIPELINE_VERSION); expect(res.body.index_templates[0]?.index_template?.composed_of).to.contain( '.fleet_component_template-1' ); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c05b15905b932..3542abf9ea863 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -22,6 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const kibanaServer = getService('kibanaServer'); async function setDiscoverTimeRange() { await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -36,8 +37,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } ); await spacesService.create({ id: 'custom_space', @@ -48,8 +53,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); @@ -84,8 +93,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); await spacesService.create({ id: 'custom_space', @@ -96,8 +105,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); diff --git a/x-pack/test/functional/apps/observability/exploratory_view.ts b/x-pack/test/functional/apps/observability/exploratory_view.ts deleted file mode 100644 index 8f27f20ce30e6..0000000000000 --- a/x-pack/test/functional/apps/observability/exploratory_view.ts +++ /dev/null @@ -1,82 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Path from 'path'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['observability', 'common', 'header']); - const esArchiver = getService('esArchiver'); - const find = getService('find'); - - const testSubjects = getService('testSubjects'); - - const rangeFrom = '2021-01-17T16%3A46%3A15.338Z'; - const rangeTo = '2021-01-19T17%3A01%3A32.309Z'; - - // Failing: See https://github.com/elastic/kibana/issues/106934 - describe.skip('ExploratoryView', () => { - before(async () => { - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data') - ); - - await PageObjects.common.navigateToApp('ux', { - search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - }); - - it('should able to open exploratory view from ux app', async () => { - await testSubjects.exists('uxAnalyzeBtn'); - await testSubjects.click('uxAnalyzeBtn'); - expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true); - }); - - it('renders lens visualization', async () => { - expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true); - - expect( - await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]') - ).to.eql(true); - - expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true); - }); - - it('can do a breakdown per series', async () => { - await testSubjects.click('seriesBreakdown'); - - expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true); - - await find.clickByCssSelector('[id="user_agent.name"]'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - - expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true); - expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true); - }); - }); -} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts index cce07b9ff7d86..b7f03b5f27bae 100644 --- a/x-pack/test/functional/apps/observability/index.ts +++ b/x-pack/test/functional/apps/observability/index.ts @@ -8,9 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('ObservabilityApp', function () { + describe('Observability specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./exploratory_view')); }); } diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json deleted file mode 100644 index af5aa6d043484..0000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:index-pattern:logstash-*", - "source": { - "namespace": "custom_space", - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:config:6.0.0", - "source": { - "namespace": "custom_space", - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json deleted file mode 100644 index 0cd1a29f92241..0000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "gis-map" : { - "properties" : { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description" : { - "type" : "text" - }, - "layerListJSON" : { - "type" : "text" - }, - "mapStateJSON" : { - "type" : "text" - }, - "title" : { - "type" : "text" - }, - "uiStateJSON" : { - "type" : "text" - }, - "version" : { - "type" : "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json new file mode 100644 index 0000000000000..d6c52e43984e2 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzYsMl0=" +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json new file mode 100644 index 0000000000000..9ddc185cf3535 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json @@ -0,0 +1,32 @@ +{ + "attributes": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "7.15.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2021-08-04T13:18:45.677Z", + "version": "WzksMl0=" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzQsMl0=" +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index afd8d1fb54cf6..aeaaf7fca1cb7 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'ml_file_data_visualizer' && catalogueId !== 'monitoring' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -74,6 +75,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'appSearch', 'workplaceSearch', 'spaces', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index b87a6475526f7..6a6b618c2c8c8 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'everything_space_all at everything_space': @@ -57,7 +57,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index d64b4f75e20a6..da4b26106afac 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'monitoring' && catalogueId !== 'ml_file_data_visualizer' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -70,6 +71,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index 4d96532b83d4a..6a44b3d8f0b71 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'read': @@ -55,7 +55,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break;