diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index 870b923f20cf4..98bbba5392b15 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -12,10 +12,10 @@ or all indices that contain your data. [[data-views-read-only-access]] === Required permissions -* Access to *Data Views* requires the <> +* Access to *Data Views* requires the <> `Data View Management`. -* To create a data view, you must have the <> +* To create a data view, you must have the <> `view_index_metadata`. * If a read-only indicator appears in {kib}, you have insufficient privileges diff --git a/docs/management/connectors/action-types/index.asciidoc b/docs/management/connectors/action-types/index.asciidoc index 7868085ef9c96..98f7dac4de81d 100644 --- a/docs/management/connectors/action-types/index.asciidoc +++ b/docs/management/connectors/action-types/index.asciidoc @@ -74,7 +74,7 @@ Example of the index document for Index Threshold rule: "rule_name": "{{ruleName}}", "alert_id": "{{alertId}}", "context_message": "{{context.message}}" -} +} -------------------------------------------------- Example of creating a test index using the API. @@ -108,7 +108,7 @@ experimental[] {kib} offers a preconfigured index connector to facilitate indexi This functionality is experimental and may be changed or removed completely in a future release. ================================================== -To use this connector, set the <> configuration to `true`. +To use this connector, set the <> configuration to `true`. ```js xpack.actions.preconfiguredAlertHistoryEsIndex: true @@ -123,11 +123,11 @@ Documents are indexed using a preconfigured schema that captures the <> for more information. +To write documents to the preconfigured index, you must have `all` or `write` privileges to the `kibana-alert-history-*` indices. Refer to <> for more information. ============================================== [NOTE] ================================================== The `kibana-alert-history-*` indices are not configured to use ILM so they must be maintained manually. If the index size grows large, consider using the {ref}/docs-delete-by-query.html[delete by query] API to clean up older documents in the index. -================================================== \ No newline at end of file +================================================== diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index c7b12c4ac32f6..58d9ca2255dd3 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -17,7 +17,7 @@ The {stack-security-features} provide roles and privileges that control which us You can manage your roles, privileges, and spaces in **{stack-manage-app}** in {kib}. For more information, see {ref}/security-privileges.html[Security privileges], -<>, and <>. +<>, and <>. To upload GeoJSON files in {kib} with *Maps*, you must have: diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 6fafe1ce2506d..5d161711719ac 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -416,27 +416,32 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. -[role="exclude",id="index-patterns-runtime-field-api-delete] +[role="exclude",id="index-patterns-runtime-field-api-delete"] == Index patterns has been renamed to data views. This content has moved. Refer to <>. -[role="exclude",id="index-patterns-runtime-field-api-get] +[role="exclude",id="index-patterns-runtime-field-api-get"] == Index patterns has been renamed to data views. This content has moved. Refer to <>. -[role="exclude",id="index-patterns-runtime-field-api-update] +[role="exclude",id="index-patterns-runtime-field-api-update"] == Index patterns has been renamed to data views. This content has moved. Refer to <>. -[role="exclude",id="index-patterns-runtime-field-api-upsert] +[role="exclude",id="index-patterns-runtime-field-api-upsert"] == Index patterns has been renamed to data views. This content has moved. Refer to <>. -[role="exclude",id="index-patterns-api-update] +[role="exclude",id="index-patterns-api-update"] == Index patterns has been renamed to data views. This content has moved. Refer to <>. + +[role="exclude",id="xpack-kibana-role-management"] +== Kibana role management. + +This content has moved. Refer to <>. diff --git a/docs/user/dashboard/aggregation-based.asciidoc b/docs/user/dashboard/aggregation-based.asciidoc index 351e1f5d0825a..c4f26e701bccf 100644 --- a/docs/user/dashboard/aggregation-based.asciidoc +++ b/docs/user/dashboard/aggregation-based.asciidoc @@ -114,7 +114,7 @@ Choose the type of visualization you want to create, then use the editor to conf . Add the <> you want to visualize using the editor, then click *Update*. + -NOTE: For the *Date Histogram* to use an *auto interval*, the date field must match the primary time field of the index pattern. +NOTE: For the *Date Histogram* to use an *auto interval*, the date field must match the primary time field of the {data-source}. . To change the order, drag and drop the aggregations in the editor. + diff --git a/docs/user/dashboard/images/lens_dataViewDropDown_8.0.png b/docs/user/dashboard/images/lens_dataViewDropDown_8.0.png new file mode 100644 index 0000000000000..309e1be49b9db Binary files /dev/null and b/docs/user/dashboard/images/lens_dataViewDropDown_8.0.png differ diff --git a/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png b/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png deleted file mode 100644 index f8e797c7dd4b6..0000000000000 Binary files a/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png and /dev/null differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 324676ecb0a8e..eaa2014717714 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -34,7 +34,7 @@ Open the visualization editor, then make sure the correct fields appear. . On the dashboard, click *Create visualization*. -. Make sure the *kibana_sample_data_ecommerce* index appears, then set the <> to *Last 30 days*. +. Make sure the *kibana_sample_data_ecommerce* {data-source} appears, then set the <> to *Last 30 days*. [discrete] [[custom-time-interval]] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 1b0bbf866b852..1fcc3eb797b59 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -48,7 +48,7 @@ Choose the data you want to visualize. . If you want to learn more about the data a field contains, click the field. -. To visualize more than one index pattern, click *Add layer > Add visualization layer*, then select the index pattern. +. To visualize more than one {data-source}, click *Add layer > Add visualization layer*, then select the {data-source}. Edit and delete. @@ -60,18 +60,18 @@ Edit and delete. [[change-the-fields]] ==== Change the fields list -Change the fields list to display a different index pattern, different time range, or add your own fields. +Change the fields list to display a different {data-source}, different time range, or add your own fields. -* To create a visualization with fields in a different index pattern, open the *Index pattern* dropdown, then select the index pattern. +* To create a visualization with fields in a different {data-source}, open the *Data view* dropdown, then select the {data-source}. * If the fields list is empty, change the <>. -* To add fields, open the action menu (*...*) next to the *Index pattern* dropdown, then select *Add field to index pattern*. +* To add fields, open the action menu (*...*) next to the *Data view* dropdown, then select *Add field to {data-source}*. + [role="screenshot"] -image:images/runtime-field-menu.png[Dropdown menu located next to index pattern field with items for adding and managing fields, width=50%] +image:images/runtime-field-menu.png[Dropdown menu located next to {data-source} field with items for adding and managing fields, width=50%] + -For more information about adding fields to index patterns and examples, refer to <>. +For more information about adding fields to {data-sources} and examples, refer to <>. [float] [[create-custom-tables]] @@ -453,7 +453,7 @@ To configure the bounds, use the menus in the editor toolbar. Bar and area chart .*Is it possible to display icons in data tables?* [%collapsible] ==== -You can display icons with <> in data tables. +You can display icons with <> in data tables. ==== [discrete] diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 8d89adc454d63..19962d11f7335 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -34,7 +34,7 @@ The fist parameter of the .es function is the parameter q (for query), which is .es(*) .es(q=*) -Multiple parameters are separated by comma. The .es function has another parameter called index, that can be used to specify an index pattern for this series, so the query won't be executed again all indexes (or whatever you changed the above mentioned setting to). +Multiple parameters are separated by a comma. The .es function has another parameter called index, that can be used to specify {a-data-source} for this series, so the query won't be executed against all indexes (or whatever you changed the setting to). .es(q=*, index=logstash-*) diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 1c90c28826f6e..a1bad870dde46 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -16,7 +16,7 @@ With *TSVB*, you can: image::images/tsvb-screenshot.png[TSVB overview] [float] -[[tsvb-index-pattern-mode]] +[[tsvb-data-view-mode]] ==== Open and set up TSVB Open *TSVB*, then configure the required settings. You can create *TSVB* visualizations with only {data-sources}, or {es} index strings. @@ -31,17 +31,19 @@ When you use only {data-sources}, you are able to: * Improve performance +[[tsvb-index-pattern-mode]] + IMPORTANT: Creating *TSVB* visualizations with an {es} index string is deprecated and will be removed in a future release. By default, you create *TSVB* visualizations with only {data-sources}. To use an {es} index string, contact your administrator, or go to <> and set `metrics:allowStringIndices` to `true`. . On the dashboard, click *All types*, then select *TSVB*. . In *TSVB*, click *Panel options*, then specify the *Data* settings. -. Open the *Index pattern selection mode* options next to the *Index pattern* dropdown. +. Open the *Data view mode* options next to the *Data view* dropdown. . Select *Use only {kib} {data-sources}*. -. From the *Index pattern* drodpown, select the {data-source}, then select the *Time field* and *Interval*. +. From the *Data view* drodpown, select the {data-source}, then select the *Time field* and *Interval*. . Select a *Drop last bucket* option. + @@ -258,9 +260,9 @@ Calculating the duration between the start and end of an event is unsupported in [%collapsible] ==== -To group with multiple fields, create runtime fields in the index pattern you are visualizing. +To group with multiple fields, create runtime fields in the {data-source} you are visualizing. -. Create a runtime field. Refer to <> for more information. +. Create a runtime field. Refer to <> for more information. + [role="screenshot"] image::images/tsvb_group_by_multiple_fields.png[Group by multiple fields] diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index e270c16cf60f6..4d36647c808b3 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -41,7 +41,7 @@ Open the visualization editor, then make sure the correct fields appear. . Make sure the *kibana_sample_data_logs* index appears. + [role="screenshot"] -image::images/lens_indexPatternDropDown_7.16.png[Index pattern dropdown] +image::images/lens_dataViewDropDown_8.0.png[Data view dropdown] To create the visualizations in this tutorial, you'll use the following fields: @@ -96,7 +96,7 @@ image::images/lens_metricUniqueVisitors_7.16.png[Metric visualization that displ There are two shortcuts you can use to view metrics over time. When you drag a numeric field to the workspace, the visualization editor adds the default -time field from the index pattern. When you use the *Date histogram* function, you can +time field from the {data-source}. When you use the *Date histogram* function, you can replace the time field by dragging the field to the workspace. To visualize the *bytes* field over time: diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 7a092b4686e2d..99cbf6f8eb533 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -202,7 +202,7 @@ Tip: Use in combination with <> helper to format date. | | context.panel.indexPatternId + context.panel.indexPatternIds -|Index pattern ids used by a panel. +|The {data-source} IDs used by a panel. | | context.panel.savedObjectId diff --git a/docs/user/dashboard/vega.asciidoc b/docs/user/dashboard/vega.asciidoc index cd893dfe8d944..fd2055a085c5e 100644 --- a/docs/user/dashboard/vega.asciidoc +++ b/docs/user/dashboard/vega.asciidoc @@ -10,7 +10,7 @@ URL, or static data, and support <> Add or delete users and assign roles that give users specific privileges. -| <> +| <> |View the roles that exist on your cluster. Customize the actions that a user with the role can perform, on a cluster, index, and space level. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index df4ad4e2b89b0..b478e8b7c38d5 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -23,7 +23,7 @@ Whichever approach you use, be careful when granting cluster privileges and inde cluster, and {kib} spaces do not prevent you from granting users of two different tenants access to the same index. [role="xpack"] -[[xpack-kibana-role-management]] +[[kibana-role-management]] === {kib} role management Roles are a collection of privileges that allow you to perform actions in {kib} and {es}. Users are not directly granted privileges, but are instead assigned one or more roles that describe the desired level of access. When you assign a user multiple roles, the user receives a union of the roles’ privileges. This means that you cannot reduce the privileges of a user by assigning them an additional role. You must instead remove or edit one of their existing roles. diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc index dd913a5bb28d8..9e457ee409f4b 100644 --- a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc +++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc @@ -131,6 +131,6 @@ This guide is an introduction to {kib}'s security features. Check out these addi * View the <> to learn more about single-sign on and other login features. -* View the <> to learn more about authorizing access to {kib}'s features. +* View the <> to learn more about authorizing access to {kib}'s features. Still have questions? Ask on our https://discuss.elastic.co/c/kibana[Kibana discuss forum] and a fellow community member or Elastic engineer will help out. diff --git a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap index 2219e0d7609b8..cc4e27a6d6388 100644 --- a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap +++ b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap @@ -4,17 +4,24 @@ exports[`MetricTile correct displays a byte metric 1`] = ` `; exports[`MetricTile correct displays a float metric 1`] = ` - `; @@ -22,7 +29,7 @@ exports[`MetricTile correct displays a time metric 1`] = ` `; @@ -31,7 +38,29 @@ exports[`MetricTile correct displays an untyped metric 1`] = ` `; + +exports[`MetricTile correctly displays a metric with metadata 1`] = ` + +`; diff --git a/src/core/public/core_app/status/components/metric_tiles.test.tsx b/src/core/public/core_app/status/components/metric_tiles.test.tsx index 76608718e8cd3..8e6d1cf38cd01 100644 --- a/src/core/public/core_app/status/components/metric_tiles.test.tsx +++ b/src/core/public/core_app/status/components/metric_tiles.test.tsx @@ -35,6 +35,18 @@ const timeMetric: Metric = { value: 1234, }; +const metricWithMeta: Metric = { + name: 'Delay', + type: 'time', + value: 1, + meta: { + description: 'Percentiles', + title: '', + value: [1, 5, 10], + type: 'time', + }, +}; + describe('MetricTile', () => { it('correct displays an untyped metric', () => { const component = shallow(); @@ -55,4 +67,9 @@ describe('MetricTile', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); + + it('correctly displays a metric with metadata', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/core/public/core_app/status/components/metric_tiles.tsx b/src/core/public/core_app/status/components/metric_tiles.tsx index 1eb5ee4c95dd8..18fa9ae738227 100644 --- a/src/core/public/core_app/status/components/metric_tiles.tsx +++ b/src/core/public/core_app/status/components/metric_tiles.tsx @@ -7,24 +7,105 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiCard } from '@elastic/eui'; -import { formatNumber, Metric } from '../lib'; +import { EuiFlexGrid, EuiFlexItem, EuiCard, EuiStat } from '@elastic/eui'; +import { DataType, formatNumber, Metric } from '../lib'; /* - * Displays a metric with the correct format. + * Displays metadata for a metric. */ -export const MetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => { - const { name } = metric; +const MetricCardFooter: FunctionComponent<{ + title: string; + description: string; +}> = ({ title, description }) => { + return ( + + ); +}; + +const DelayMetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => { + const { name, meta } = metric; return ( + ) + } + /> + ); +}; + +const LoadMetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => { + const { name, meta } = metric; + return ( + } /> ); }; +const ResponseTimeMetric: FunctionComponent<{ metric: Metric }> = ({ metric }) => { + const { name, meta } = metric; + return ( + + ) + } + /> + ); +}; + +/* + * Displays a metric with the correct format. + */ +export const MetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => { + const { name } = metric; + switch (name) { + case 'Delay': + return ; + case 'Load': + return ; + case 'Response time avg': + return ; + default: + return ( + + ); + } +}; + /* * Wrapper component that simply maps each metric to MetricTile inside a FlexGroup */ @@ -38,11 +119,20 @@ export const MetricTiles: FunctionComponent<{ metrics: Metric[] }> = ({ metrics ); +// formatting helper functions + const formatMetric = ({ value, type }: Metric) => { const metrics = Array.isArray(value) ? value : [value]; return metrics.map((metric) => formatNumber(metric, type)).join(', '); }; -const formatMetricId = ({ name }: Metric) => { +const formatMetricId = (name: Metric['name']) => { return name.toLowerCase().replace(/[ ]+/g, '-'); }; + +const formatDelayFooterTitle = (values: number[], type?: DataType) => { + return ` + 50: ${formatNumber(values[0], type)}; + 95: ${formatNumber(values[1], type)}; + 99: ${formatNumber(values[2], type)}`; +}; diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts index 555e793b41aa5..f044aa2daa2e9 100644 --- a/src/core/public/core_app/status/lib/load_status.test.ts +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -229,13 +229,23 @@ describe('response processing', () => { expect(names).toEqual([ 'Heap total', 'Heap used', + 'Requests per second', 'Load', + 'Delay', 'Response time avg', - 'Response time max', - 'Requests per second', ]); - const values = data.metrics.map((m) => m.value); - expect(values).toEqual([1000000, 100, [4.1, 2.1, 0.1], 4000, 8000, 400]); + expect(values).toEqual([1000000, 100, 400, [4.1, 2.1, 0.1], 1, 4000]); + }); + + test('adds meta details to Load, Delay and Response time', async () => { + const data = await loadStatus({ http, notifications }); + const metricNames = data.metrics.filter((met) => met.meta); + expect(metricNames.map((item) => item.name)).toEqual(['Load', 'Delay', 'Response time avg']); + expect(metricNames.map((item) => item.meta!.description)).toEqual([ + 'Load interval', + 'Percentiles', + 'Response time max', + ]); }); }); diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts index 31f20bf5c4edf..2d81d51128926 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -13,10 +13,17 @@ import type { HttpSetup } from '../../../http'; import type { NotificationsSetup } from '../../../notifications'; import type { DataType } from '../lib'; +interface MetricMeta { + title: string; + description: string; + value?: number[]; + type?: DataType; +} export interface Metric { name: string; value: number | number[]; type?: DataType; + meta?: MetricMeta; } export interface FormattedStatus { @@ -60,33 +67,62 @@ function formatMetrics({ metrics }: StatusResponse): Metric[] { value: metrics.process.memory.heap.used_in_bytes, type: 'byte', }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.requestsPerSecHeader', { + defaultMessage: 'Requests per second', + }), + value: (metrics.requests.total * 1000) / metrics.collection_interval_in_millis, + type: 'float', + }, { name: i18n.translate('core.statusPage.metricsTiles.columns.loadHeader', { defaultMessage: 'Load', }), value: [metrics.os.load['1m'], metrics.os.load['5m'], metrics.os.load['15m']], type: 'float', + meta: { + description: i18n.translate('core.statusPage.metricsTiles.columns.load.metaHeader', { + defaultMessage: 'Load interval', + }), + title: Object.keys(metrics.os.load).join('; '), + }, }, { - name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeAvgHeader', { - defaultMessage: 'Response time avg', + name: i18n.translate('core.statusPage.metricsTiles.columns.processDelayHeader', { + defaultMessage: 'Delay', }), - value: metrics.response_times.avg_in_millis, + value: metrics.process.event_loop_delay, type: 'time', + meta: { + description: i18n.translate( + 'core.statusPage.metricsTiles.columns.processDelayDetailsHeader', + { + defaultMessage: 'Percentiles', + } + ), + title: '', + value: [ + metrics.process.event_loop_delay_histogram?.percentiles['50'], + metrics.process.event_loop_delay_histogram?.percentiles['95'], + metrics.process.event_loop_delay_histogram?.percentiles['99'], + ], + type: 'time', + }, }, { - name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeMaxHeader', { - defaultMessage: 'Response time max', + name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeAvgHeader', { + defaultMessage: 'Response time avg', }), - value: metrics.response_times.max_in_millis, + value: metrics.response_times.avg_in_millis, type: 'time', - }, - { - name: i18n.translate('core.statusPage.metricsTiles.columns.requestsPerSecHeader', { - defaultMessage: 'Requests per second', - }), - value: (metrics.requests.total * 1000) / metrics.collection_interval_in_millis, - type: 'float', + meta: { + description: i18n.translate('core.statusPage.metricsTiles.columns.resTimeMaxHeader', { + defaultMessage: 'Response time max', + }), + title: '', + value: [metrics.response_times.max_in_millis], + type: 'time', + }, }, ]; } diff --git a/test/functional/apps/status_page/index.ts b/test/functional/apps/status_page/index.ts index 99f32fa5da4c7..509abeb4f0346 100644 --- a/test/functional/apps/status_page/index.ts +++ b/test/functional/apps/status_page/index.ts @@ -40,6 +40,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(metrics).to.have.length(6); }); + it('should display the server metrics meta', async () => { + const metricsMetas = await testSubjects.findAll('serverMetricMeta'); + expect(metricsMetas).to.have.length(3); + }); + it('should display the server status', async () => { const titleText = await testSubjects.getVisibleText('serverStatusTitle'); expect(titleText).to.contain('Kibana status is'); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index b84e4faf2925e..46a346e4b9d19 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -180,9 +180,11 @@ export const WorkpadHeader: FC = ({ - - - + {isWriteable && ( + + + + )} diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index 9c14dd088665a..1a334448e9208 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -215,10 +215,6 @@ export type Category = { count: number; }; -export type CategoryFieldMeta = { - categories: Category[]; -}; - export type GeometryTypes = { isPointsOnly: boolean; isLinesOnly: boolean; @@ -228,7 +224,7 @@ export type GeometryTypes = { export type FieldMeta = { [key: string]: { range?: RangeFieldMeta; - categories?: CategoryFieldMeta; + categories: Category[]; }; }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index e0ccb80273c9d..710824d0e1414 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -233,13 +233,11 @@ test('Should pluck the categorical style-meta', async () => { const features = makeFeatures(['CN', 'CN', 'US', 'CN', 'US', 'IN']); const meta = colorStyle.pluckCategoricalStyleMetaFromFeatures(features); - expect(meta).toEqual({ - categories: [ - { key: 'CN', count: 3 }, - { key: 'US', count: 2 }, - { key: 'IN', count: 1 }, - ], - }); + expect(meta).toEqual([ + { key: 'CN', count: 3 }, + { key: 'US', count: 2 }, + { key: 'IN', count: 1 }, + ]); }); test('Should pluck the categorical style-meta from fieldmeta', async () => { @@ -262,13 +260,11 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => { }, }); - expect(meta).toEqual({ - categories: [ - { key: 'CN', count: 3 }, - { key: 'US', count: 2 }, - { key: 'IN', count: 1 }, - ], - }); + expect(meta).toEqual([ + { key: 'CN', count: 3 }, + { key: 'US', count: 2 }, + { key: 'IN', count: 1 }, + ]); }); describe('supportsFieldMeta', () => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index 03800fa03827e..c9e7cfb6d7e39 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -263,8 +263,8 @@ export class DynamicColorProperty extends DynamicStyleProperty extends IStyleProperty { getMbFieldName(): string; getFieldOrigin(): FIELD_ORIGIN | null; getRangeFieldMeta(): RangeFieldMeta | null; - getCategoryFieldMeta(): CategoryFieldMeta | null; + getCategoryFieldMeta(): Category[]; /* * Returns hash that signals style meta needs to be re-fetched when value changes */ @@ -57,11 +57,9 @@ export interface IDynamicStyleProperty extends IStyleProperty { supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; - pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; + pluckCategoricalStyleMetaFromFeatures(features: Feature[]): Category[]; pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures: TileMetaFeature[]): RangeFieldMeta | null; - pluckCategoricalStyleMetaFromTileMetaFeatures( - features: TileMetaFeature[] - ): CategoryFieldMeta | null; + pluckCategoricalStyleMetaFromTileMetaFeatures(features: TileMetaFeature[]): Category[]; getValueSuggestions(query: string): Promise; enrichGeoJsonAndMbFeatureState( featureCollection: FeatureCollection, @@ -175,19 +173,19 @@ export class DynamicStyleProperty _getCategoryFieldMetaFromStyleMetaRequest() { const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName()); if (!dataRequestId) { - return null; + return []; } const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { - return null; + return []; } const data = styleMetaDataRequest.getData() as StyleMetaData; return this._pluckCategoricalStyleMetaFromFieldMetaData(data); } - getCategoryFieldMeta(): CategoryFieldMeta | null { + getCategoryFieldMeta(): Category[] { const categoryFieldMetaFromLocalFeatures = this._getCategoryFieldMetaFromLocalFeatures(); if (!this.isFieldMetaEnabled()) { @@ -195,7 +193,7 @@ export class DynamicStyleProperty } const categoricalFieldMetaFromServer = this._getCategoryFieldMetaFromStyleMetaRequest(); - return categoricalFieldMetaFromServer + return categoricalFieldMetaFromServer.length ? categoricalFieldMetaFromServer : categoryFieldMetaFromLocalFeatures; } @@ -332,10 +330,8 @@ export class DynamicStyleProperty }; } - pluckCategoricalStyleMetaFromTileMetaFeatures( - metaFeatures: TileMetaFeature[] - ): CategoryFieldMeta | null { - return null; + pluckCategoricalStyleMetaFromTileMetaFeatures(metaFeatures: TileMetaFeature[]): Category[] { + return []; } pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null { @@ -364,10 +360,10 @@ export class DynamicStyleProperty }; } - pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null { + pluckCategoricalStyleMetaFromFeatures(features: Feature[]): Category[] { const size = this.getNumberOfCategories(); if (!this.isCategorical() || size <= 0) { - return null; + return []; } const counts = new Map(); @@ -384,7 +380,7 @@ export class DynamicStyleProperty } } - const ordered = []; + const ordered: Category[] = []; for (const [key, value] of counts) { ordered.push({ key, count: value }); } @@ -392,10 +388,7 @@ export class DynamicStyleProperty ordered.sort((a, b) => { return b.count - a.count; }); - const truncated = ordered.slice(0, size); - return { - categories: truncated, - } as CategoryFieldMeta; + return ordered.slice(0, size); } _pluckOrdinalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData): RangeFieldMeta | null { @@ -422,26 +415,22 @@ export class DynamicStyleProperty }; } - _pluckCategoricalStyleMetaFromFieldMetaData( - styleMetaData: StyleMetaData - ): CategoryFieldMeta | null { + _pluckCategoricalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData): Category[] { if (!this.isCategorical() || !this._field) { - return null; + return []; } const fieldMeta = styleMetaData[`${this._field.getRootName()}_terms`]; if (!fieldMeta || !('buckets' in fieldMeta)) { - return null; + return []; } - return { - categories: fieldMeta.buckets.map((bucket) => { - return { - key: bucket.key, - count: bucket.doc_count, - }; - }), - }; + return fieldMeta.buckets.map((bucket) => { + return { + key: bucket.key, + count: bucket.doc_count, + }; + }); } formatField(value: RawValue): string | number { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts index 9d6560ecb8888..13455b3e4f840 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts @@ -8,12 +8,6 @@ // eslint-disable-next-line max-classes-per-file import { FIELD_ORIGIN, LAYER_STYLE_TYPE } from '../../../../../../common/constants'; import { StyleMeta } from '../../style_meta'; -import { - CategoryFieldMeta, - GeometryTypes, - RangeFieldMeta, - StyleMetaDescriptor, -} from '../../../../../../common/descriptor_types'; import { AbstractField, IField } from '../../../../fields/field'; import { IStyle } from '../../../style'; @@ -77,40 +71,32 @@ export class MockStyle implements IStyle { } getStyleMeta(): StyleMeta { - const geomTypes: GeometryTypes = { - isPointsOnly: false, - isLinesOnly: false, - isPolygonsOnly: false, - }; - const rangeFieldMeta: RangeFieldMeta = { - min: this._min, - max: this._max, - delta: this._max - this._min, - }; - const catFieldMeta: CategoryFieldMeta = { - categories: [ - { - key: 'US', - count: 10, - }, - { - key: 'CN', - count: 8, - }, - ], - }; - - const styleMetaDescriptor: StyleMetaDescriptor = { - geometryTypes: geomTypes, + return new StyleMeta({ + geometryTypes: { + isPointsOnly: false, + isLinesOnly: false, + isPolygonsOnly: false, + }, fieldMeta: { foobar: { - range: rangeFieldMeta, - categories: catFieldMeta, + range: { + min: this._min, + max: this._max, + delta: this._max - this._min, + }, + categories: [ + { + key: 'US', + count: 10, + }, + { + key: 'CN', + count: 8, + }, + ], }, }, - }; - - return new StyleMeta(styleMetaDescriptor); + }); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts index 14fab5ca93748..5177cdb814833 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - StyleMetaDescriptor, - RangeFieldMeta, - CategoryFieldMeta, -} from '../../../../common/descriptor_types'; +import { StyleMetaDescriptor, RangeFieldMeta, Category } from '../../../../common/descriptor_types'; export class StyleMeta { private readonly _descriptor: StyleMetaDescriptor; @@ -23,10 +19,8 @@ export class StyleMeta { : null; } - getCategoryFieldMetaDescriptor(fieldName: string): CategoryFieldMeta | null { - return this._descriptor.fieldMeta[fieldName] && this._descriptor.fieldMeta[fieldName].categories - ? this._descriptor.fieldMeta[fieldName].categories! - : null; + getCategoryFieldMetaDescriptor(fieldName: string): Category[] { + return this._descriptor.fieldMeta[fieldName].categories; } isPointsOnly(): boolean { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index a4ea62cb63970..bb83d6b1eb448 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -511,19 +511,15 @@ export class VectorStyle implements IVectorStyle { } dynamicProperties.forEach((dynamicProperty) => { - const ordinalStyleMeta = - dynamicProperty.pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures); - const categoricalStyleMeta = - dynamicProperty.pluckCategoricalStyleMetaFromTileMetaFeatures(metaFeatures); - const name = dynamicProperty.getFieldName(); if (!styleMeta.fieldMeta[name]) { - styleMeta.fieldMeta[name] = {}; - } - if (categoricalStyleMeta) { - styleMeta.fieldMeta[name].categories = categoricalStyleMeta; + styleMeta.fieldMeta[name] = { categories: [] }; } + styleMeta.fieldMeta[name].categories = + dynamicProperty.pluckCategoricalStyleMetaFromTileMetaFeatures(metaFeatures); + const ordinalStyleMeta = + dynamicProperty.pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures); if (ordinalStyleMeta) { styleMeta.fieldMeta[name].range = ordinalStyleMeta; } @@ -595,17 +591,13 @@ export class VectorStyle implements IVectorStyle { dynamicProperties.forEach( (dynamicProperty: IDynamicStyleProperty) => { - const categoricalStyleMeta = - dynamicProperty.pluckCategoricalStyleMetaFromFeatures(features); - const ordinalStyleMeta = dynamicProperty.pluckOrdinalStyleMetaFromFeatures(features); const name = dynamicProperty.getFieldName(); if (!styleMeta.fieldMeta[name]) { - styleMeta.fieldMeta[name] = {}; - } - if (categoricalStyleMeta) { - styleMeta.fieldMeta[name].categories = categoricalStyleMeta; + styleMeta.fieldMeta[name] = { categories: [] }; } - + styleMeta.fieldMeta[name].categories = + dynamicProperty.pluckCategoricalStyleMetaFromFeatures(features); + const ordinalStyleMeta = dynamicProperty.pluckOrdinalStyleMetaFromFeatures(features); if (ordinalStyleMeta) { styleMeta.fieldMeta[name].range = ordinalStyleMeta; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx index 561126f3264ad..4fce0361d5d13 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx @@ -18,6 +18,11 @@ import { AlertsCountAggregation } from './types'; jest.mock('../../../../common/lib/kibana'); const mockDispatch = jest.fn(); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); return { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx index bd747fb637cb8..cd407a125cdb6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx @@ -6,10 +6,9 @@ */ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; -import type { AlertsStackByField } from '../common/types'; export const getAlertsCountQuery = ( - stackByField: AlertsStackByField, + stackByField: string, from: string, to: string, additionalFilters: Array<{ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx index 9660916d4f32c..d0b05587a4711 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -13,6 +13,11 @@ import { TestProviders } from '../../../../common/mock'; import { AlertsCountPanel } from './index'; +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + describe('AlertsCountPanel', () => { const defaultProps = { signalIndexName: 'signalIndexName', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 94b09c4a5ea21..b69f4f1f498f9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -21,8 +21,7 @@ import * as i18n from './translations'; import { AlertsCount } from './alerts_count'; import type { AlertsCountAggregation } from './types'; import { DEFAULT_STACK_BY_FIELD } from '../common/config'; -import type { AlertsStackByField } from '../common/types'; -import { KpiPanel, StackBySelect } from '../common/components'; +import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; @@ -39,8 +38,7 @@ export const AlertsCountPanel = memo( // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COUNT_ID}-${uuid.v4()}`, []); - const [selectedStackByOption, setSelectedStackByOption] = - useState(DEFAULT_STACK_BY_FIELD); + const [selectedStackByOption, setSelectedStackByOption] = useState(DEFAULT_STACK_BY_FIELD); // TODO: Once we are past experimental phase this code should be removed // const fetchMethod = useIsExperimentalFeatureEnabled('ruleRegistryEnabled') @@ -99,7 +97,7 @@ export const AlertsCountPanel = memo( titleSize="s" hideSubtitle > - + { ...originalModule, createHref: jest.fn(), useHistory: jest.fn(), + useLocation: jest.fn().mockReturnValue({ pathname: '' }), }; }); @@ -37,9 +38,21 @@ jest.mock('../../../../common/lib/kibana/kibana_react', () => { navigateToApp: mockNavigateToApp, getUrlForApp: jest.fn(), }, + data: { + search: { + search: jest.fn(), + }, + }, uiSettings: { get: jest.fn(), }, + notifications: { + toasts: { + addWarning: jest.fn(), + addError: jest.fn(), + addSuccess: jest.fn(), + }, + }, }, }), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 873b5d40184ef..11dbb4da863db 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -41,7 +41,7 @@ import { LinkButton } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; import { DEFAULT_STACK_BY_FIELD, PANEL_HEIGHT } from '../common/config'; import type { AlertsStackByField } from '../common/types'; -import { KpiPanel, StackBySelect } from '../common/components'; +import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; @@ -109,7 +109,7 @@ export const AlertsHistogramPanel = memo( const [isInspectDisabled, setIsInspectDisabled] = useState(false); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const [totalAlertsObj, setTotalAlertsObj] = useState(defaultTotalAlertsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState( + const [selectedStackByOption, setSelectedStackByOption] = useState( onlyField == null ? defaultStackByOption : onlyField ); @@ -276,10 +276,12 @@ export const AlertsHistogramPanel = memo( {showStackBy && ( - + <> + + )} {headerChildren != null && headerChildren} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx index 53d41835d6bb9..6a56f7bc220ac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { EuiPanel, EuiSelect } from '@elastic/eui'; +import { EuiPanel, EuiComboBox } from '@elastic/eui'; import styled from 'styled-components'; -import React, { useCallback } from 'react'; -import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT, alertsStackByOptions } from './config'; -import type { AlertsStackByField } from './types'; +import React, { useCallback, useMemo } from 'react'; +import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT } from './config'; +import { useStackByFields } from './hooks'; import * as i18n from './translations'; export const KpiPanel = styled(EuiPanel)<{ height?: number }>` @@ -25,24 +25,45 @@ export const KpiPanel = styled(EuiPanel)<{ height?: number }>` } `; interface StackedBySelectProps { - selected: AlertsStackByField; - onSelect: (selected: AlertsStackByField) => void; + selected: string; + onSelect: (selected: string) => void; } -export const StackBySelect: React.FC = ({ selected, onSelect }) => { - const setSelectedOptionCallback = useCallback( - (event: React.ChangeEvent) => { - onSelect(event.target.value as AlertsStackByField); +export const StackByComboBoxWrapper = styled.div` + width: 400px; +`; + +export const StackByComboBox: React.FC = ({ selected, onSelect }) => { + const onChange = useCallback( + (options) => { + if (options && options.length > 0) { + onSelect(options[0].value); + } else { + onSelect(''); + } }, [onSelect] ); - + const selectedOptions = useMemo(() => { + return [{ label: selected, value: selected }]; + }, [selected]); + const stackOptions = useStackByFields(); + const singleSelection = useMemo(() => { + return { asPlainText: true }; + }, []); return ( - + + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx index ad0fc1fa7ac61..d68c5c303cfd7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx @@ -5,8 +5,16 @@ * 2.0. */ +import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { useInspectButton, UseInspectButtonParams } from './hooks'; +import { useInspectButton, UseInspectButtonParams, useStackByFields } from './hooks'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { TestProviders } from '../../../../common/mock'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); describe('hooks', () => { describe('useInspectButton', () => { @@ -43,4 +51,22 @@ describe('hooks', () => { expect(mockDeleteQuery).toHaveBeenCalledWith({ id: defaultParams.uniqueQueryId }); }); }); + + describe('useStackByFields', () => { + jest.mock('../../../../common/containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ browserFields: mockBrowserFields }), + })); + it('returns only aggregateable fields', () => { + const wrapper = ({ children }: { children: JSX.Element }) => ( + {children} + ); + const { result, unmount } = renderHook(() => useStackByFields(), { wrapper }); + const aggregateableFields = result.current; + unmount(); + expect(aggregateableFields?.find((field) => field.label === 'agent.id')).toBeTruthy(); + expect( + aggregateableFields?.find((field) => field.label === 'nestedField.firstAttributes') + ).toBe(undefined); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts index 6375e2b0c27fb..65b87670810b0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { useEffect } from 'react'; +import { useEffect, useState, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { BrowserField } from '../../../../../../timelines/common'; import type { GlobalTimeArgs } from '../../../../common/containers/use_global_time'; +import { getScopeFromPath, useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { getAllFieldsByName } from '../../../../common/containers/source'; export interface UseInspectButtonParams extends Pick { response: string; @@ -15,6 +20,7 @@ export interface UseInspectButtonParams extends Pick }) { + return Object.entries(fields).reduce( + (filteredOptions: EuiComboBoxOptionOption[], [key, field]) => { + if (field.aggregatable === true) { + return [...filteredOptions, { label: key, value: key }]; + } else { + return filteredOptions; + } + }, + [] + ); +} + +export const useStackByFields = () => { + const { pathname } = useLocation(); + + const { browserFields } = useSourcererDataView(getScopeFromPath(pathname)); + const allFields = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); + const [stackByFieldOptions, setStackByFieldOptions] = useState(() => + getAggregatableFields(allFields) + ); + useEffect(() => { + setStackByFieldOptions(getAggregatableFields(allFields)); + }, [allFields]); + return useMemo(() => stackByFieldOptions, [stackByFieldOptions]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts index d99e1d4744ae7..d45563675154e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts @@ -13,3 +13,17 @@ export const STACK_BY_LABEL = i18n.translate( defaultMessage: 'Stack by', } ); + +export const STACK_BY_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByPlaceholder', + { + defaultMessage: 'Select a field to stack by', + } +); + +export const STACK_BY_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByAriaLabel', + { + defaultMessage: 'Stack the alerts histogram by a field value', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index eeb22a2aa071c..c5d053c57fc97 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -23,6 +23,7 @@ import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../common/mock/router'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -71,6 +72,9 @@ jest.mock('../../../common/lib/kibana', () => { siem: { crud_alerts: true, read_alerts: true }, }, }, + uiSettings: { + get: jest.fn(), + }, timelines: { ...mockTimelines }, data: { query: { @@ -113,6 +117,7 @@ describe('DetectionEnginePageComponent', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, + browserFields: mockBrowserFields, }); }); diff --git a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts index a392df276a34c..ba65cd8d62465 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts @@ -60,7 +60,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should render the "Data" section with ILM', async () => { await PageObjects.common.navigateToApp('management'); const sections = await managementMenu.getSections(); - expect(sections).to.have.length(1); + // Changed sections to have a length of 2 because of + // https://github.com/elastic/kibana/pull/121262 + expect(sections).to.have.length(2); expect(sections[0]).to.eql({ sectionId: 'data', sectionLinks: ['index_lifecycle_management'], diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index 95ddd0a7b5944..d13aeab4d2334 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -16,9 +16,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); const retry = getService('retry'); const esClient = getService('es'); + const security = getService('security'); describe('Home page', function () { before(async () => { + await security.testUser.setRoles(['manage_ilm'], true); await esClient.snapshot.createRepository({ name: repoName, body: { @@ -30,11 +32,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }, verify: false, }); + await pageObjects.common.navigateToApp('indexLifecycleManagement'); }); after(async () => { await esClient.snapshot.deleteRepository({ name: repoName }); await esClient.ilm.deleteLifecycle({ name: policyName }); + await security.testUser.restoreDefaults(); }); it('Loads the app', async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index de0c5dbd3699f..6812fc97181e6 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -516,6 +516,14 @@ export default async function ({ readConfigFile }) { elasticsearch: { cluster: ['manage_ilm'], }, + kibana: [ + { + feature: { + advancedSettings: ['read'], + }, + spaces: ['default'], + }, + ], }, index_management_user: {