From 17043d4f9d699855852841449dfe2f6c4ad377e4 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 21 Jan 2021 11:25:02 -0800 Subject: [PATCH 01/60] Use doc link service in Stack Monitoring (#88920) --- src/core/public/doc_links/doc_links_service.ts | 5 +++++ .../public/alerts/ccr_read_exceptions_alert/index.tsx | 2 +- .../public/alerts/cpu_usage_alert/cpu_usage_alert.tsx | 2 +- .../monitoring/public/alerts/disk_usage_alert/index.tsx | 2 +- .../monitoring/public/alerts/legacy_alert/legacy_alert.tsx | 2 +- .../monitoring/public/alerts/memory_usage_alert/index.tsx | 2 +- .../missing_monitoring_data_alert.tsx | 2 +- .../public/alerts/thread_pool_rejections_alert/index.tsx | 2 +- 8 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 1a69c7db35a73..b82254e5a1416 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -182,7 +182,12 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, }, monitoring: { + alertsCluster: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/cluster-alerts.html`, alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, + alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, + alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, + alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, + alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, }, diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index 6d7751d91b761..e656c0ab253e0 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -37,7 +37,7 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel ( diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index d2cec006b1b1d..9b207457683f6 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -16,7 +16,7 @@ export function createCpuUsageAlertType(): AlertTypeModel ( diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index bea399ee89f6a..aeb9bab2aae9a 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -18,7 +18,7 @@ export function createDiskUsageAlertType(): AlertTypeModel ( diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index d50e9c3a5c282..4a3532ad61240 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -18,7 +18,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { description: LEGACY_ALERT_DETAILS[legacyAlert].description, iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/cluster-alerts.html`; + return `${docLinks.links.monitoring.alertsCluster}`; }, alertParamsExpression: () => ( diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 0428e4e7c733e..b484cd9a975fd 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -18,7 +18,7 @@ export function createMemoryUsageAlertType(): AlertTypeModel ( diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx index fdb89033c4e2c..18a4990eeaaa7 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -16,7 +16,7 @@ export function createMissingMonitoringDataAlertType(): AlertTypeModel { description: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].description, iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`; + return `${docLinks.links.monitoring.alertsKibanaMissingData}`; }, alertParamsExpression: (props: any) => ( ( <> From 76b23f17e2035185aa7bd213af2767b564302a1d Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 21 Jan 2021 14:32:35 -0500 Subject: [PATCH 02/60] add custom metrics to node tooltip (#88545) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../conditional_tooltip.test.tsx.snap | 42 +++++++++- .../waffle/conditional_tooltip.test.tsx | 82 ++++++++++++++++++- .../components/waffle/conditional_tooltip.tsx | 32 ++++++-- 3 files changed, 143 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap index b8cdc0acac1dc..a5d97813e4b14 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap @@ -21,9 +21,10 @@ exports[`ConditionalToolTip should just work 1`] = ` host-01 CPU usage @@ -35,9 +36,10 @@ exports[`ConditionalToolTip should just work 1`] = ` Memory usage @@ -49,9 +51,10 @@ exports[`ConditionalToolTip should just work 1`] = ` Outbound traffic @@ -63,9 +66,10 @@ exports[`ConditionalToolTip should just work 1`] = ` Inbound traffic @@ -76,5 +80,35 @@ exports[`ConditionalToolTip should just work 1`] = ` 8Mbit/s + + + My Custom Label + + + 34.1% + + + + + Avg of host.network.out.packets + + + 4,392.2 + + `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index e01ca3ab6e844..fbca85e2d4496 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -22,7 +22,12 @@ jest.mock('../../../../../containers/source', () => ({ jest.mock('../../hooks/use_snaphot'); import { useSnapshot } from '../../hooks/use_snaphot'; +jest.mock('../../hooks/use_waffle_options'); +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; const mockedUseSnapshot = useSnapshot as jest.Mock>; +const mockedUseWaffleOptionsContext = useWaffleOptionsContext as jest.Mock< + ReturnType +>; const NODE: InfraWaffleMapNode = { pathId: 'host-01', @@ -50,6 +55,7 @@ const ChildComponent = () =>
child
; describe('ConditionalToolTip', () => { afterEach(() => { mockedUseSnapshot.mockReset(); + mockedUseWaffleOptionsContext.mockReset(); }); function createWrapper(currentTime: number = Date.now(), hidden: boolean = false) { @@ -77,6 +83,7 @@ describe('ConditionalToolTip', () => { interval: '', reload: jest.fn(() => Promise.resolve()), }); + mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); const currentTime = Date.now(); const wrapper = createWrapper(currentTime, true); expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); @@ -95,6 +102,18 @@ describe('ConditionalToolTip', () => { { name: 'memory', value: 0.8, avg: 0.8, max: 1 }, { name: 'tx', value: 1000000, avg: 1000000, max: 1000000 }, { name: 'rx', value: 1000000, avg: 1000000, max: 1000000 }, + { + name: 'cedd6ca0-5775-11eb-a86f-adb714b6c486', + max: 0.34164999922116596, + value: 0.34140000740687054, + avg: 0.20920833365784752, + }, + { + name: 'e12dd700-5775-11eb-a86f-adb714b6c486', + max: 4703.166666666667, + value: 4392.166666666667, + avg: 3704.6666666666674, + }, ], }, ], @@ -103,6 +122,7 @@ describe('ConditionalToolTip', () => { interval: '60s', reload: reloadMock, }); + mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); const currentTime = Date.now(); const wrapper = createWrapper(currentTime, false); expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); @@ -114,7 +134,25 @@ describe('ConditionalToolTip', () => { }, }, }); - const expectedMetrics = [{ type: 'cpu' }, { type: 'memory' }, { type: 'tx' }, { type: 'rx' }]; + const expectedMetrics = [ + { type: 'cpu' }, + { type: 'memory' }, + { type: 'tx' }, + { type: 'rx' }, + { + aggregation: 'avg', + field: 'host.cpu.pct', + id: 'cedd6ca0-5775-11eb-a86f-adb714b6c486', + label: 'My Custom Label', + type: 'custom', + }, + { + aggregation: 'avg', + field: 'host.network.out.packets', + id: 'e12dd700-5775-11eb-a86f-adb714b6c486', + type: 'custom', + }, + ]; expect(mockedUseSnapshot).toBeCalledWith( expectedQuery, expectedMetrics, @@ -143,6 +181,7 @@ describe('ConditionalToolTip', () => { interval: '', reload: reloadMock, }); + mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); const currentTime = Date.now(); const wrapper = createWrapper(currentTime, false); expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); @@ -154,3 +193,44 @@ describe('ConditionalToolTip', () => { expect(reloadMock).not.toHaveBeenCalled(); }); }); + +const mockedUseWaffleOptionsContexReturnValue: ReturnType = { + changeMetric: jest.fn(() => {}), + changeGroupBy: jest.fn(() => {}), + changeNodeType: jest.fn(() => {}), + changeView: jest.fn(() => {}), + changeCustomOptions: jest.fn(() => {}), + changeAutoBounds: jest.fn(() => {}), + changeBoundsOverride: jest.fn(() => {}), + changeAccount: jest.fn(() => {}), + changeRegion: jest.fn(() => {}), + changeCustomMetrics: jest.fn(() => {}), + changeLegend: jest.fn(() => {}), + changeSort: jest.fn(() => {}), + setWaffleOptionsState: jest.fn(() => {}), + boundsOverride: { max: 1, min: 0 }, + autoBounds: true, + accountId: '', + region: '', + sort: { by: 'name', direction: 'desc' }, + groupBy: [], + nodeType: 'host', + customOptions: [], + view: 'map', + metric: { type: 'cpu' }, + customMetrics: [ + { + aggregation: 'avg', + field: 'host.cpu.pct', + id: 'cedd6ca0-5775-11eb-a86f-adb714b6c486', + label: 'My Custom Label', + type: 'custom', + }, + { + aggregation: 'avg', + field: 'host.network.out.packets', + id: 'e12dd700-5775-11eb-a86f-adb714b6c486', + type: 'custom', + }, + ], +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index 8082752a88b7f..7ec1ae905a640 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -6,6 +6,8 @@ import React, { useCallback, useState, useEffect } from 'react'; import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { first } from 'lodash'; +import { getCustomMetricLabel } from '../../../../../../common/formatters/get_custom_metric_label'; +import { SnapshotCustomMetricInput } from '../../../../../../common/http_api'; import { withTheme, EuiTheme } from '../../../../../../../observability/public'; import { useSourceContext } from '../../../../../containers/source'; import { findInventoryModel } from '../../../../../../common/inventory_models'; @@ -18,6 +20,8 @@ import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/li import { useSnapshot } from '../../hooks/use_snaphot'; import { createInventoryMetricFormatter } from '../../lib/create_inventory_metric_formatter'; import { SNAPSHOT_METRIC_TRANSLATIONS } from '../../../../../../common/inventory_models/intl_strings'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { createFormatterForMetric } from '../../../metrics_explorer/components/helpers/create_formatter_for_metric'; export interface Props { currentTime: number; @@ -35,9 +39,15 @@ export const ConditionalToolTip = withTheme( const { sourceId } = useSourceContext(); const [timer, setTimer] = useState | null>(null); const model = findInventoryModel(nodeType); - const requestMetrics = model.tooltipMetrics.map((type) => ({ type })) as Array<{ - type: SnapshotMetricType; - }>; + const { customMetrics } = useWaffleOptionsContext(); + const requestMetrics = model.tooltipMetrics + .map((type) => ({ type })) + .concat(customMetrics) as Array< + | { + type: SnapshotMetricType; + } + | SnapshotCustomMetricInput + >; const query = JSON.stringify({ bool: { filter: { @@ -45,7 +55,6 @@ export const ConditionalToolTip = withTheme( }, }, }); - const { nodes, reload } = useSnapshot( query, requestMetrics, @@ -74,7 +83,6 @@ export const ConditionalToolTip = withTheme( if (hidden) { return children; } - const dataNode = first(nodes); const metrics = (dataNode && dataNode.metrics) || []; const content = ( @@ -91,10 +99,18 @@ export const ConditionalToolTip = withTheme( {metrics.map((metric) => { const metricName = SnapshotMetricTypeRT.is(metric.name) ? metric.name : 'custom'; const name = SNAPSHOT_METRIC_TRANSLATIONS[metricName] || metricName; - const formatter = createInventoryMetricFormatter({ type: metricName }); + // if custom metric, find field and label from waffleOptionsContext result + // because useSnapshot does not return it + const customMetric = + name === 'custom' ? customMetrics.find((item) => item.id === metric.name) : null; + const formatter = customMetric + ? createFormatterForMetric(customMetric) + : createInventoryMetricFormatter({ type: metricName }); return ( - - {name} + + + {customMetric ? getCustomMetricLabel(customMetric) : name} + {(metric.value && formatter(metric.value)) || '-'} From ae0bd2fbba07901445260a0cbd264da6017eb152 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 21 Jan 2021 14:10:19 -0600 Subject: [PATCH 03/60] Add runtime fields to index patterns and searchsource (#88542) * Add runtime fields to index patterns and searchsource --- ...ata-public.indexpattern.addruntimefield.md | 25 +++++ ...ublic.indexpattern.getassavedobjectbody.md | 2 + ...a-public.indexpattern.getcomputedfields.md | 2 + ...plugin-plugins-data-public.indexpattern.md | 2 + ...-public.indexpattern.removeruntimefield.md | 24 +++++ ...gins-data-public.indexpatternattributes.md | 1 + ....indexpatternattributes.runtimefieldmap.md | 11 +++ ...-data-public.indexpatternfield.ismapped.md | 13 +++ ...n-plugins-data-public.indexpatternfield.md | 2 + ...a-public.indexpatternfield.runtimefield.md | 13 +++ ...in-plugins-data-public.indexpatternspec.md | 1 + ...public.indexpatternspec.runtimefieldmap.md | 11 +++ ...ata-server.indexpattern.addruntimefield.md | 25 +++++ ...erver.indexpattern.getassavedobjectbody.md | 2 + ...a-server.indexpattern.getcomputedfields.md | 2 + ...plugin-plugins-data-server.indexpattern.md | 2 + ...-server.indexpattern.removeruntimefield.md | 24 +++++ ...gins-data-server.indexpatternattributes.md | 1 + ....indexpatternattributes.runtimefieldmap.md | 11 +++ .../index_pattern_field.test.ts.snap | 7 ++ .../fields/index_pattern_field.test.ts | 8 +- .../fields/index_pattern_field.ts | 20 +++- .../__snapshots__/index_pattern.test.ts.snap | 93 +++++++++++++++++++ .../__snapshots__/index_patterns.test.ts.snap | 1 + .../fixtures/logstash_fields.js | 1 + .../index_patterns/index_pattern.test.ts | 86 ++++++++++++++++- .../index_patterns/index_pattern.ts | 57 +++++++++++- .../index_patterns/index_patterns.ts | 29 +++++- .../data/common/index_patterns/types.ts | 12 +++ .../search_source/search_source.test.ts | 22 ++++- .../search/search_source/search_source.ts | 15 ++- src/plugins/data/public/public.api.md | 17 +++- src/plugins/data/server/server.api.md | 13 ++- .../helpers/get_sharing_data.test.ts | 1 + .../test/functional/apps/maps/mvt_scaling.js | 2 +- .../functional/apps/maps/mvt_super_fine.js | 2 +- 36 files changed, 542 insertions(+), 18 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md new file mode 100644 index 0000000000000..5640395139ba6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [addRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md) + +## IndexPattern.addRuntimeField() method + +Add a runtime field - Appended to existing mapped field or a new field is created as appropriate + +Signature: + +```typescript +addRuntimeField(name: string, runtimeField: RuntimeField): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| runtimeField | RuntimeField | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index b318427012c0a..48d94b84497bd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -20,6 +20,7 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; ``` Returns: @@ -35,5 +36,6 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md index 84aeb9ffeb21a..37d31a35167df 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md @@ -14,6 +14,7 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }; ``` Returns: @@ -25,5 +26,6 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 872e23e450f88..53d173d39f50d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -45,6 +45,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | +| [addRuntimeField(name, runtimeField)](./kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md) | | Add a runtime field - Appended to existing mapped field or a new field is created as appropriate | | [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | @@ -58,6 +59,7 @@ export declare class IndexPattern implements IIndexPattern | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md new file mode 100644 index 0000000000000..7a5228fece782 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [removeRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) + +## IndexPattern.removeRuntimeField() method + +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate + +Signature: + +```typescript +removeRuntimeField(name: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 297bfa855f0eb..41a4d3c55694b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -21,6 +21,7 @@ export interface IndexPatternAttributes | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md) | string | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternattributes.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md new file mode 100644 index 0000000000000..0df7a9841e41f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md) + +## IndexPatternAttributes.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md new file mode 100644 index 0000000000000..653a1f2b39c29 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [isMapped](./kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md) + +## IndexPatternField.isMapped property + +Is the field part of the index mapping? + +Signature: + +```typescript +get isMapped(): boolean | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index c8118770ed394..05c807b1cd845 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -27,9 +27,11 @@ export declare class IndexPatternField implements IFieldType | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | +| [isMapped](./kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md) | | boolean | undefined | Is the field part of the index mapping? | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | Script field language | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | +| [runtimeField](./kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md) | | RuntimeField | undefined | | | [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | Script field code | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md new file mode 100644 index 0000000000000..ad3b81eb23edc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [runtimeField](./kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md) + +## IndexPatternField.runtimeField property + +Signature: + +```typescript +get runtimeField(): RuntimeField | undefined; + +set runtimeField(runtimeField: RuntimeField | undefined); +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index c0fa165cfb115..ae514e3fc6a8a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -22,6 +22,7 @@ export interface IndexPatternSpec | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | | [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | saved object id | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md) | Record<string, RuntimeField> | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md new file mode 100644 index 0000000000000..e208760ff188f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md) + +## IndexPatternSpec.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md new file mode 100644 index 0000000000000..ebd7f46d3598e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [addRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md) + +## IndexPattern.addRuntimeField() method + +Add a runtime field - Appended to existing mapped field or a new field is created as appropriate + +Signature: + +```typescript +addRuntimeField(name: string, runtimeField: RuntimeField): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| runtimeField | RuntimeField | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index 7d70af4b535fe..668d563ff04c0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -20,6 +20,7 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; ``` Returns: @@ -35,5 +36,6 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md index eab6ae9bf9033..0030adf1261e4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md @@ -14,6 +14,7 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }; ``` Returns: @@ -25,5 +26,6 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 70c37ba1b3926..97d1cd9115262 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -45,6 +45,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | +| [addRuntimeField(name, runtimeField)](./kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md) | | Add a runtime field - Appended to existing mapped field or a new field is created as appropriate | | [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | @@ -58,6 +59,7 @@ export declare class IndexPattern implements IIndexPattern | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md new file mode 100644 index 0000000000000..da8e7e40a7fac --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [removeRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) + +## IndexPattern.removeRuntimeField() method + +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate + +Signature: + +```typescript +removeRuntimeField(name: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index bfc7f65425f9c..20af97ecc8761 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -21,6 +21,7 @@ export interface IndexPatternAttributes | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md) | string | | | [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-server.indexpatternattributes.title.md) | string | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md new file mode 100644 index 0000000000000..1e0dff2ad0e46 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md) + +## IndexPatternAttributes.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: string; +``` diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 3e09fa449a1aa..4ef61ec0f2557 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -57,9 +57,16 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": undefined, "lang": "lang", "name": "name", "readFromDocValues": false, + "runtimeField": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, "script": "script", "scripted": true, "searchable": true, diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index bce75f9932479..8a73abb3c7d83 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -9,7 +9,7 @@ import { IndexPatternField } from './index_pattern_field'; import { IndexPattern } from '../index_patterns'; import { KBN_FIELD_TYPES, FieldFormat } from '../../../common'; -import { FieldSpec } from '../types'; +import { FieldSpec, RuntimeField } from '../types'; describe('Field', function () { function flatten(obj: Record) { @@ -42,6 +42,12 @@ describe('Field', function () { } as unknown) as IndexPattern, $$spec: ({} as unknown) as FieldSpec, conflictDescriptions: { a: ['b', 'c'], d: ['e'] }, + runtimeField: { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world')", + }, + }, }; it('the correct properties are writable', () => { diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 540563c3a8cfc..ed6c4bd40d561 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -6,9 +6,10 @@ * Public License, v 1. */ +import type { RuntimeField } from '../types'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { IFieldType } from './types'; +import type { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; import { shortenDottedString } from '../../utils'; @@ -35,6 +36,14 @@ export class IndexPatternField implements IFieldType { this.spec.count = count; } + public get runtimeField() { + return this.spec.runtimeField; + } + + public set runtimeField(runtimeField: RuntimeField | undefined) { + this.spec.runtimeField = runtimeField; + } + /** * Script field code */ @@ -117,6 +126,13 @@ export class IndexPatternField implements IFieldType { return this.spec.subType; } + /** + * Is the field part of the index mapping? + */ + public get isMapped() { + return this.spec.isMapped; + } + // not writable, not serialized public get sortable() { return ( @@ -181,6 +197,8 @@ export class IndexPatternField implements IFieldType { format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, customLabel: this.customLabel, shortDotsEnable: this.spec.shortDotsEnable, + runtimeField: this.runtimeField, + isMapped: this.isMapped, }; } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 76de2b2662bb0..4aadddfad3b97 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -20,9 +20,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "@tags", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -44,9 +46,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "@timestamp", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -68,9 +72,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_id", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -92,9 +98,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_source", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -116,9 +124,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_type", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -140,9 +150,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "area", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -164,9 +176,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "bytes", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -188,9 +202,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "custom_user_field", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -212,9 +228,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "extension", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -236,9 +254,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "extension.keyword", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -264,9 +284,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "geo.coordinates", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -288,9 +310,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "geo.src", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -312,9 +336,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "hashed", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -336,9 +362,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "ip", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -360,9 +388,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "machine.os", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -384,9 +414,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "machine.os.raw", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -412,9 +444,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "non-filterable", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": false, @@ -436,9 +470,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "non-sortable", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": false, @@ -460,9 +496,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "phpmemory", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -484,9 +522,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "point", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -508,9 +548,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "request_body", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -518,6 +560,35 @@ Object { "subType": undefined, "type": "attachment", }, + "runtime_field": Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "customLabel": undefined, + "esTypes": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "isMapped": undefined, + "lang": undefined, + "name": "runtime_field", + "readFromDocValues": false, + "runtimeField": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, + "script": undefined, + "scripted": false, + "searchable": false, + "shortDotsEnable": false, + "subType": undefined, + "type": undefined, + }, "script date": Object { "aggregatable": true, "conflictDescriptions": undefined, @@ -532,9 +603,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "painless", "name": "script date", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -556,9 +629,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script murmur3", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -580,9 +655,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script number", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -604,9 +681,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script string", "readFromDocValues": false, + "runtimeField": undefined, "script": "'i am a string'", "scripted": true, "searchable": true, @@ -628,9 +707,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "ssl", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -652,9 +733,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "time", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -676,9 +759,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "utc_time", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -689,6 +774,14 @@ Object { }, "id": "test-pattern", "intervalName": undefined, + "runtimeFieldMap": Object { + "runtime_field": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, + }, "sourceFilters": undefined, "timeFieldName": "timestamp", "title": "title", diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap index bad74430b8966..d6da4adac81a4 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -10,6 +10,7 @@ Object { "fields": Object {}, "id": "id", "intervalName": undefined, + "runtimeFieldMap": Object {}, "sourceFilters": Array [ Object { "value": "item1", diff --git a/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js index 3e81b9234ee64..2bcb8df34cf02 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js +++ b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js @@ -68,6 +68,7 @@ function stubbedLogstashFields() { lang, scripted, subType, + isMapped: !scripted, }; }); } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index bb7ed17f9e608..4f6e83460aecf 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -18,9 +18,27 @@ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; import { FieldFormat } from '../..'; +import { RuntimeField } from '../types'; class MockFieldFormatter {} +const runtimeFieldScript = { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world')", + }, +}; + +const runtimeFieldMap = { + runtime_field: runtimeFieldScript, +}; + +const runtimeField = { + name: 'runtime_field', + runtimeField: runtimeFieldScript, + scripted: false, +}; + fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; // helper function to create index patterns @@ -32,7 +50,15 @@ function create(id: string) { } = stubbedSavedObjectIndexPattern(id); return new IndexPattern({ - spec: { id, type, version, timeFieldName, fields, title }, + spec: { + id, + type, + version, + timeFieldName, + fields: { ...fields, runtime_field: runtimeField }, + title, + runtimeFieldMap, + }, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -53,6 +79,10 @@ describe('IndexPattern', () => { expect(indexPattern).toHaveProperty('getNonScriptedFields'); expect(indexPattern).toHaveProperty('addScriptedField'); expect(indexPattern).toHaveProperty('removeScriptedField'); + expect(indexPattern).toHaveProperty('addScriptedField'); + expect(indexPattern).toHaveProperty('removeScriptedField'); + expect(indexPattern).toHaveProperty('addRuntimeField'); + expect(indexPattern).toHaveProperty('removeRuntimeField'); // properties expect(indexPattern).toHaveProperty('fields'); @@ -65,6 +95,7 @@ describe('IndexPattern', () => { expect(indexPattern.fields[0]).toHaveProperty('filterable'); expect(indexPattern.fields[0]).toHaveProperty('sortable'); expect(indexPattern.fields[0]).toHaveProperty('scripted'); + expect(indexPattern.fields[0]).toHaveProperty('isMapped'); }); }); @@ -98,6 +129,12 @@ describe('IndexPattern', () => { expect(docValueFieldNames).toContain('utc_time'); }); + test('should return runtimeField', () => { + expect(indexPattern.getComputedFields().runtimeFields).toEqual({ + runtime_field: runtimeFieldScript, + }); + }); + test('should request date field doc values in date_time format', () => { const { docvalueFields } = indexPattern.getComputedFields(); const timestampField = docvalueFields.find((field) => field.field === '@timestamp'); @@ -117,6 +154,7 @@ describe('IndexPattern', () => { const notScriptedNames = mockLogStashFields() .filter((item: IndexPatternField) => item.scripted === false) .map((item: IndexPatternField) => item.name); + notScriptedNames.push('runtime_field'); const respNames = map(indexPattern.getNonScriptedFields(), 'name'); expect(respNames).toEqual(notScriptedNames); @@ -185,6 +223,52 @@ describe('IndexPattern', () => { }); }); + describe('addRuntimeField and removeRuntimeField', () => { + const runtime = { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world');", + }, + }; + + beforeEach(() => { + const formatter = { + toJSON: () => ({ id: 'bytes' }), + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; + }); + + test('add and remove runtime field to existing field', () => { + indexPattern.addRuntimeField('@tags', runtime); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + '@tags': runtime, + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!['@tags'].runtimeField).toEqual(runtime); + + indexPattern.removeRuntimeField('@tags'); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!['@tags'].runtimeField).toBeUndefined(); + }); + + test('add and remove runtime field as new field', () => { + indexPattern.addRuntimeField('new_field', runtime); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + new_field: runtime, + }); + expect(indexPattern.toSpec()!.fields!.new_field.runtimeField).toEqual(runtime); + + indexPattern.removeRuntimeField('new_field'); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!.new_field).toBeUndefined(); + }); + }); + describe('getFormatterForField', () => { test('should return the default one for empty objects', () => { indexPattern.setFieldFormat('scriptedFieldWithEmptyFormatter', {}); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 452c663d96716..144d38fe15909 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -8,6 +8,7 @@ import _, { each, reject } from 'lodash'; import { FieldAttrs, FieldAttrSet } from '../..'; +import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -17,6 +18,7 @@ import { flattenHitWrapper } from './flatten_hit'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; import { IndexPatternSpec, TypeMeta, SourceFilter, IndexPatternFieldMap } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; +import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; interface IndexPatternDeps { spec?: IndexPatternSpec; @@ -74,6 +76,8 @@ export class IndexPattern implements IIndexPattern { private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; private fieldAttrs: FieldAttrs; + private runtimeFieldMap: Record; + /** * prevents errors when index pattern exists before indices */ @@ -115,6 +119,7 @@ export class IndexPattern implements IIndexPattern { this.fieldAttrs = spec.fieldAttrs || {}; this.intervalName = spec.intervalName; this.allowNoIndex = spec.allowNoIndex || false; + this.runtimeFieldMap = spec.runtimeFieldMap || {}; } /** @@ -160,7 +165,8 @@ export class IndexPattern implements IIndexPattern { return { storedFields: ['*'], scriptFields, - docvalueFields: [], + docvalueFields: [] as Array<{ field: string; format: string }>, + runtimeFields: {}, }; } @@ -192,6 +198,7 @@ export class IndexPattern implements IIndexPattern { storedFields: ['*'], scriptFields, docvalueFields, + runtimeFields: this.runtimeFieldMap, }; } @@ -210,6 +217,7 @@ export class IndexPattern implements IIndexPattern { typeMeta: this.typeMeta, type: this.type, fieldFormats: this.fieldFormatMap, + runtimeFieldMap: this.runtimeFieldMap, fieldAttrs: this.fieldAttrs, intervalName: this.intervalName, allowNoIndex: this.allowNoIndex, @@ -305,6 +313,7 @@ export class IndexPattern implements IIndexPattern { ? undefined : JSON.stringify(this.fieldFormatMap); const fieldAttrs = this.getFieldAttrs(); + const runtimeFieldMap = this.runtimeFieldMap; return { fieldAttrs: fieldAttrs ? JSON.stringify(fieldAttrs) : undefined, @@ -319,6 +328,7 @@ export class IndexPattern implements IIndexPattern { type: this.type, typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, allowNoIndex: this.allowNoIndex ? this.allowNoIndex : undefined, + runtimeFieldMap: runtimeFieldMap ? JSON.stringify(runtimeFieldMap) : undefined, }; } @@ -340,6 +350,51 @@ export class IndexPattern implements IIndexPattern { ); } + /** + * Add a runtime field - Appended to existing mapped field or a new field is + * created as appropriate + * @param name Field name + * @param runtimeField Runtime field definition + */ + + addRuntimeField(name: string, runtimeField: RuntimeField) { + const existingField = this.getFieldByName(name); + if (existingField) { + existingField.runtimeField = runtimeField; + } else { + this.fields.add({ + name, + runtimeField, + type: castEsToKbnFieldTypeName(runtimeField.type), + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); + } + this.runtimeFieldMap[name] = runtimeField; + } + + /** + * Remove a runtime field - removed from mapped field or removed unmapped + * field as appropriate + * @param name Field name + */ + + removeRuntimeField(name: string) { + const existingField = this.getFieldByName(name); + if (existingField) { + if (existingField.isMapped) { + // mapped field, remove runtimeField def + existingField.runtimeField = undefined; + } else { + // runtimeField only + this.fields.remove(existingField); + } + } + delete this.runtimeFieldMap[name]; + } + /** * Get formatter for a given field name. Return undefined if none exists * @param field diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 80cb8a55fa0a0..60436da530b63 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -11,6 +11,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; +import type { RuntimeField } from '../types'; import { IndexPattern } from './index_pattern'; import { createEnsureDefaultIndexPattern, @@ -34,6 +35,7 @@ import { SavedObjectNotFound } from '../../../../kibana_utils/common'; import { IndexPatternMissingIndices } from '../lib'; import { findByTitle } from '../utils'; import { DuplicateIndexPatternError } from '../errors'; +import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; @@ -247,7 +249,8 @@ export class IndexPatternsService { */ refreshFields = async (indexPattern: IndexPattern) => { try { - const fields = await this.getFieldsForIndexPattern(indexPattern); + const fields = (await this.getFieldsForIndexPattern(indexPattern)) as FieldSpec[]; + fields.forEach((field) => (field.isMapped = true)); const scripted = indexPattern.getScriptedFields().map((field) => field.spec); const fieldAttrs = indexPattern.getFieldAttrs(); const fieldsWithSavedAttrs = Object.values( @@ -288,6 +291,7 @@ export class IndexPatternsService { try { let updatedFieldList: FieldSpec[]; const newFields = (await this.getFieldsForWildcard(options)) as FieldSpec[]; + newFields.forEach((field) => (field.isMapped = true)); // If allowNoIndex, only update field list if field caps finds fields. To support // beats creating index pattern and dashboard before docs @@ -347,6 +351,7 @@ export class IndexPatternsService { fields, sourceFilters, fieldFormatMap, + runtimeFieldMap, typeMeta, type, fieldAttrs, @@ -359,6 +364,9 @@ export class IndexPatternsService { const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; const parsedFieldAttrs: FieldAttrs = fieldAttrs ? JSON.parse(fieldAttrs) : {}; + const parsedRuntimeFieldMap: Record = runtimeFieldMap + ? JSON.parse(runtimeFieldMap) + : {}; return { id, @@ -373,6 +381,7 @@ export class IndexPatternsService { fieldFormats: parsedFieldFormatMap, fieldAttrs: parsedFieldAttrs, allowNoIndex, + runtimeFieldMap: parsedRuntimeFieldMap, }; }; @@ -387,7 +396,7 @@ export class IndexPatternsService { } const spec = this.savedObjectToSpec(savedObject); - const { title, type, typeMeta } = spec; + const { title, type, typeMeta, runtimeFieldMap } = spec; spec.fieldAttrs = savedObject.attributes.fieldAttrs ? JSON.parse(savedObject.attributes.fieldAttrs) : {}; @@ -406,6 +415,22 @@ export class IndexPatternsService { }, spec.fieldAttrs ); + // APPLY RUNTIME FIELDS + for (const [key, value] of Object.entries(runtimeFieldMap || {})) { + if (spec.fields[key]) { + spec.fields[key].runtimeField = value; + } else { + spec.fields[key] = { + name: key, + type: castEsToKbnFieldTypeName(value.type), + runtimeField: value, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }; + } + } } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 9f9a26604a0e5..467b5125f0327 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -14,6 +14,14 @@ import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; +const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export interface RuntimeField { + type: RuntimeType; + script: { + source: string; + }; +} /** * IIndexPattern allows for an IndexPattern OR an index pattern saved object @@ -51,6 +59,7 @@ export interface IndexPatternAttributes { sourceFilters?: string; fieldFormatMap?: string; fieldAttrs?: string; + runtimeFieldMap?: string; /** * prevents errors when index pattern exists before indices */ @@ -199,8 +208,10 @@ export interface FieldSpec { subType?: IFieldSubType; indexed?: boolean; customLabel?: string; + runtimeField?: RuntimeField; // not persisted shortDotsEnable?: boolean; + isMapped?: boolean; } export type IndexPatternFieldMap = Record; @@ -230,6 +241,7 @@ export interface IndexPatternSpec { typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; + runtimeFieldMap?: Record; fieldAttrs?: FieldAttrs; allowNoIndex?: boolean; } diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 0b9c60e94a198..6d7654c6659f2 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -20,6 +20,7 @@ const getComputedFields = () => ({ storedFields: [], scriptFields: {}, docvalueFields: [], + runtimeFields: {}, }); const mockSource = { excludes: ['foo-*'] }; @@ -37,6 +38,13 @@ const indexPattern2 = ({ getSourceFiltering: () => mockSource2, } as unknown) as IndexPattern; +const runtimeFieldDef = { + type: 'keyword', + script: { + source: "emit('hello world')", + }, +}; + describe('SearchSource', () => { let mockSearchMethod: any; let searchSourceDependencies: SearchSourceDependencies; @@ -82,12 +90,14 @@ describe('SearchSource', () => { describe('computed fields handling', () => { test('still provides computed fields when no fields are specified', async () => { + const runtimeFields = { runtime_field: runtimeFieldDef }; searchSource.setField('index', ({ ...indexPattern, getComputedFields: () => ({ storedFields: ['hello'], scriptFields: { world: {} }, docvalueFields: ['@timestamp'], + runtimeFields, }), } as unknown) as IndexPattern); @@ -95,6 +105,7 @@ describe('SearchSource', () => { expect(request.stored_fields).toEqual(['hello']); expect(request.script_fields).toEqual({ world: {} }); expect(request.fields).toEqual(['@timestamp']); + expect(request.runtime_mappings).toEqual(runtimeFields); }); test('never includes docvalue_fields', async () => { @@ -390,15 +401,23 @@ describe('SearchSource', () => { }); test('filters request when a specific list of fields is provided with fieldsFromSource', async () => { + const runtimeFields = { runtime_field: runtimeFieldDef, runtime_field_b: runtimeFieldDef }; searchSource.setField('index', ({ ...indexPattern, getComputedFields: () => ({ storedFields: ['*'], scriptFields: { hello: {}, world: {} }, docvalueFields: ['@timestamp', 'date'], + runtimeFields, }), } as unknown) as IndexPattern); - searchSource.setField('fieldsFromSource', ['hello', '@timestamp', 'foo-a', 'bar']); + searchSource.setField('fieldsFromSource', [ + 'hello', + '@timestamp', + 'foo-a', + 'bar', + 'runtime_field', + ]); const request = await searchSource.getSearchRequestBody(); expect(request._source).toEqual({ @@ -407,6 +426,7 @@ describe('SearchSource', () => { expect(request.fields).toEqual(['@timestamp']); expect(request.script_fields).toEqual({ hello: {} }); expect(request.stored_fields).toEqual(['@timestamp', 'bar']); + expect(request.runtime_mappings).toEqual({ runtime_field: runtimeFieldDef }); }); test('filters request when a specific list of fields is provided with fieldsFromSource or fields', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 0f0688c9fc11f..554e8385881f2 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -461,12 +461,13 @@ export class SearchSource { searchRequest.indexType = this.getIndexType(index); // get some special field types from the index pattern - const { docvalueFields, scriptFields, storedFields } = index + const { docvalueFields, scriptFields, storedFields, runtimeFields } = index ? index.getComputedFields() : { docvalueFields: [], scriptFields: {}, storedFields: ['*'], + runtimeFields: {}, }; const fieldListProvided = !!body.fields; @@ -481,6 +482,7 @@ export class SearchSource { ...scriptFields, }; body.stored_fields = storedFields; + body.runtime_mappings = runtimeFields || {}; // apply source filters from index pattern if specified by the user let filteredDocvalueFields = docvalueFields; @@ -518,13 +520,18 @@ export class SearchSource { body.script_fields, Object.keys(body.script_fields).filter((f) => uniqFieldNames.includes(f)) ); + body.runtime_mappings = pick( + body.runtime_mappings, + Object.keys(body.runtime_mappings).filter((f) => uniqFieldNames.includes(f)) + ); } // request the remaining fields from stored_fields just in case, since the // fields API does not handle stored fields - const remainingFields = difference(uniqFieldNames, Object.keys(body.script_fields)).filter( - Boolean - ); + const remainingFields = difference(uniqFieldNames, [ + ...Object.keys(body.script_fields), + ...Object.keys(body.runtime_mappings), + ]).filter(Boolean); // only include unique values body.stored_fields = [...new Set(remainingFields)]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6bd896a584a4..28997de4517e7 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1265,6 +1265,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addRuntimeField(name: string, runtimeField: RuntimeField): void; addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -1304,6 +1305,7 @@ export class IndexPattern implements IIndexPattern { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; // (undocumented) getComputedFields(): { @@ -1313,6 +1315,7 @@ export class IndexPattern implements IIndexPattern { field: any; format: string; }[]; + runtimeFields: Record; }; // (undocumented) getFieldAttrs: () => { @@ -1352,6 +1355,7 @@ export class IndexPattern implements IIndexPattern { isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; + removeRuntimeField(name: string): void; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) @@ -1402,6 +1406,8 @@ export interface IndexPatternAttributes { // (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: string; + // (undocumented) sourceFilters?: string; // (undocumented) timeFieldName?: string; @@ -1435,12 +1441,16 @@ export class IndexPatternField implements IFieldType { get esTypes(): string[] | undefined; // (undocumented) get filterable(): boolean; + get isMapped(): boolean | undefined; get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) get name(): string; // (undocumented) get readFromDocValues(): boolean; + // (undocumented) + get runtimeField(): RuntimeField | undefined; + set runtimeField(runtimeField: RuntimeField | undefined); get script(): string | undefined; set script(script: string | undefined); // (undocumented) @@ -1537,6 +1547,8 @@ export interface IndexPatternSpec { // @deprecated (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: Record; + // (undocumented) sourceFilters?: SourceFilter[]; // (undocumented) timeFieldName?: string; @@ -2580,8 +2592,9 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/search_source/search_source.ts:186:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ef8015ecaca26..6a96fd8209a8d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -705,6 +705,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addRuntimeField(name: string, runtimeField: RuntimeField): void; addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -746,6 +747,7 @@ export class IndexPattern implements IIndexPattern { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; // (undocumented) getComputedFields(): { @@ -755,6 +757,7 @@ export class IndexPattern implements IIndexPattern { field: any; format: string; }[]; + runtimeFields: Record; }; // (undocumented) getFieldAttrs: () => { @@ -796,6 +799,7 @@ export class IndexPattern implements IIndexPattern { isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; + removeRuntimeField(name: string): void; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) @@ -838,6 +842,8 @@ export interface IndexPatternAttributes { // (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: string; + // (undocumented) sourceFilters?: string; // (undocumented) timeFieldName?: string; @@ -1394,9 +1400,10 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:50:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:52:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:46:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index f72d65dd2ee56..1394ceab1dd18 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -49,6 +49,7 @@ describe('getSharingData', () => { "should": Array [], }, }, + "runtime_mappings": Object {}, "script_fields": Object {}, "sort": Array [ Object { diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index b5c9ddcbd5e13..a7551aca78b52 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -29,7 +29,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect(mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0]).to.equal( - '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:(includes:!(geometry,prop1)),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape' + '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:(includes:!(geometry,prop1)),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape' ); //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index 3de2f461bc855..aede736deb262 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -32,7 +32,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect(mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0]).to.equal( - "/api/maps/mvt/getGridTile?x={x}&y={y}&z={z}&geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point" + "/api/maps/mvt/getGridTile?x={x}&y={y}&z={z}&geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point" ); //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) From f6689729eae7349ddddc535826385dca948cdff8 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 21 Jan 2021 21:10:52 +0100 Subject: [PATCH 04/60] Migrate authentication functionality to a new Elasticsearch client. (#87094) --- .../authentication_service.mock.ts | 8 +- .../authentication_service.test.ts | 133 +-- .../authentication/authentication_service.ts | 107 +-- .../authentication/authenticator.test.ts | 26 +- .../server/authentication/authenticator.ts | 10 +- .../security/server/authentication/index.ts | 6 +- .../providers/anonymous.test.ts | 61 +- .../authentication/providers/anonymous.ts | 5 +- .../authentication/providers/base.mock.ts | 2 +- .../server/authentication/providers/base.ts | 12 +- .../authentication/providers/basic.test.ts | 42 +- .../authentication/providers/http.test.ts | 33 +- .../authentication/providers/kerberos.test.ts | 169 ++-- .../authentication/providers/kerberos.ts | 40 +- .../authentication/providers/oidc.test.ts | 256 +++--- .../server/authentication/providers/oidc.ts | 55 +- .../authentication/providers/pki.test.ts | 161 ++-- .../server/authentication/providers/pki.ts | 15 +- .../authentication/providers/saml.test.ts | 485 ++++++---- .../server/authentication/providers/saml.ts | 65 +- .../authentication/providers/token.test.ts | 93 +- .../server/authentication/providers/token.ts | 14 +- .../server/authentication/tokens.test.ts | 252 +++--- .../security/server/authentication/tokens.ts | 24 +- .../elasticsearch_client_plugin.ts | 214 ----- .../elasticsearch_service.test.ts | 64 +- .../elasticsearch/elasticsearch_service.ts | 38 +- .../security/server/elasticsearch/index.ts | 3 +- x-pack/plugins/security/server/mocks.ts | 2 +- x-pack/plugins/security/server/plugin.test.ts | 7 +- x-pack/plugins/security/server/plugin.ts | 116 ++- .../security/server/routes/index.mock.ts | 2 +- .../plugins/security/server/routes/index.ts | 2 +- .../routes/session_management/info.test.ts | 4 +- .../server/routes/session_management/info.ts | 4 +- .../routes/users/change_password.test.ts | 5 +- .../server/routes/users/change_password.ts | 4 +- .../routes/views/access_agreement.test.ts | 4 +- .../server/routes/views/access_agreement.ts | 4 +- .../server/routes/views/logged_out.test.ts | 3 +- .../server/routes/views/logged_out.ts | 4 +- .../server/session_management/index.ts | 2 +- .../session_management/session_index.test.ts | 845 +++++++++--------- .../session_management/session_index.ts | 83 +- .../session_management_service.test.ts | 132 +-- .../session_management_service.ts | 58 +- 46 files changed, 1809 insertions(+), 1865 deletions(-) delete mode 100644 x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index 06884611f3873..9c67cf611eb44 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -5,17 +5,11 @@ */ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; -import type { - AuthenticationServiceSetup, - AuthenticationServiceStart, -} from './authentication_service'; +import type { AuthenticationServiceStart } from './authentication_service'; import { apiKeysMock } from './api_keys/api_keys.mock'; export const authenticationServiceMock = { - createSetup: (): jest.Mocked => ({ - getCurrentUser: jest.fn(), - }), createStart: (): DeeplyMockedKeys => ({ apiKeys: apiKeysMock.create(), login: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 59771c5027012..942ddc202360b 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -25,11 +25,9 @@ import { sessionMock } from '../session_management/session.mock'; import type { AuthenticationHandler, AuthToolkit, - ILegacyClusterClient, KibanaRequest, Logger, LoggerFactory, - LegacyScopedClusterClient, HttpServiceSetup, HttpServiceStart, } from '../../../../../src/core/server'; @@ -46,47 +44,17 @@ describe('AuthenticationService', () => { let service: AuthenticationService; let logger: jest.Mocked; let mockSetupAuthenticationParams: { - legacyAuditLogger: jest.Mocked; - audit: jest.Mocked; - config: ConfigType; - loggers: LoggerFactory; http: jest.Mocked; - clusterClient: jest.Mocked; license: jest.Mocked; - getFeatureUsageService: () => jest.Mocked; - session: jest.Mocked>; }; - let mockScopedClusterClient: jest.Mocked>; beforeEach(() => { logger = loggingSystemMock.createLogger(); mockSetupAuthenticationParams = { - legacyAuditLogger: securityAuditLoggerMock.create(), - audit: auditServiceMock.create(), http: coreMock.createSetup().http, - config: createConfig( - ConfigSchema.validate({ - encryptionKey: 'ab'.repeat(16), - secureCookies: true, - cookieName: 'my-sid-cookie', - }), - loggingSystemMock.create().get(), - { isTLSEnabled: false } - ), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), license: licenseMock.create(), - loggers: loggingSystemMock.create(), - getFeatureUsageService: jest - .fn() - .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), - session: sessionMock.create(), }; - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockSetupAuthenticationParams.clusterClient.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); - service = new AuthenticationService(logger); }); @@ -101,6 +69,42 @@ describe('AuthenticationService', () => { expect.any(Function) ); }); + }); + + describe('#start()', () => { + let mockStartAuthenticationParams: { + legacyAuditLogger: jest.Mocked; + audit: jest.Mocked; + config: ConfigType; + loggers: LoggerFactory; + http: jest.Mocked; + clusterClient: ReturnType; + featureUsageService: jest.Mocked; + session: jest.Mocked>; + }; + beforeEach(() => { + const coreStart = coreMock.createStart(); + mockStartAuthenticationParams = { + legacyAuditLogger: securityAuditLoggerMock.create(), + audit: auditServiceMock.create(), + config: createConfig( + ConfigSchema.validate({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + cookieName: 'my-sid-cookie', + }), + loggingSystemMock.create().get(), + { isTLSEnabled: false } + ), + http: coreStart.http, + clusterClient: elasticsearchServiceMock.createClusterClient(), + loggers: loggingSystemMock.create(), + featureUsageService: securityFeatureUsageServiceMock.createStartContract(), + session: sessionMock.create(), + }; + + service.setup(mockSetupAuthenticationParams); + }); describe('authentication handler', () => { let authHandler: AuthenticationHandler; @@ -109,12 +113,7 @@ describe('AuthenticationService', () => { beforeEach(() => { mockAuthToolkit = httpServiceMock.createAuthToolkit(); - service.setup(mockSetupAuthenticationParams); - - expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1); - expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith( - expect.any(Function) - ); + service.start(mockStartAuthenticationParams); authHandler = mockSetupAuthenticationParams.http.registerAuth.mock.calls[0][0]; authenticate = jest.requireMock('./authenticator').Authenticator.mock.instances[0] @@ -298,63 +297,7 @@ describe('AuthenticationService', () => { describe('getCurrentUser()', () => { let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null; beforeEach(async () => { - getCurrentUser = service.setup(mockSetupAuthenticationParams).getCurrentUser; - }); - - it('returns `null` if Security is disabled', () => { - mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false); - - expect(getCurrentUser(httpServerMock.createKibanaRequest())).toBe(null); - }); - - it('returns user from the auth state.', () => { - const mockUser = mockAuthenticatedUser(); - - const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock; - mockAuthGet.mockReturnValue({ state: mockUser }); - - const mockRequest = httpServerMock.createKibanaRequest(); - expect(getCurrentUser(mockRequest)).toBe(mockUser); - expect(mockAuthGet).toHaveBeenCalledTimes(1); - expect(mockAuthGet).toHaveBeenCalledWith(mockRequest); - }); - - it('returns null if auth state is not available.', () => { - const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock; - mockAuthGet.mockReturnValue({}); - - const mockRequest = httpServerMock.createKibanaRequest(); - expect(getCurrentUser(mockRequest)).toBeNull(); - expect(mockAuthGet).toHaveBeenCalledTimes(1); - expect(mockAuthGet).toHaveBeenCalledWith(mockRequest); - }); - }); - }); - - describe('#start()', () => { - let mockStartAuthenticationParams: { - http: jest.Mocked; - clusterClient: ReturnType; - }; - beforeEach(() => { - const coreStart = coreMock.createStart(); - mockStartAuthenticationParams = { - http: coreStart.http, - clusterClient: elasticsearchServiceMock.createClusterClient(), - }; - service.setup(mockSetupAuthenticationParams); - }); - - describe('getCurrentUser()', () => { - let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null; - beforeEach(async () => { - getCurrentUser = (await service.start(mockStartAuthenticationParams)).getCurrentUser; - }); - - it('returns `null` if Security is disabled', () => { - mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false); - - expect(getCurrentUser(httpServerMock.createKibanaRequest())).toBe(null); + getCurrentUser = service.start(mockStartAuthenticationParams).getCurrentUser; }); it('returns user from the auth state.', () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index e435ae43f3bf3..3ab92d0bd211f 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -11,7 +11,6 @@ import type { Logger, HttpServiceSetup, IClusterClient, - ILegacyClusterClient, HttpServiceStart, } from '../../../../../src/core/server'; import type { SecurityLicense } from '../../common/licensing'; @@ -27,27 +26,19 @@ import { APIKeys } from './api_keys'; import { Authenticator, ProviderLoginAttempt } from './authenticator'; interface AuthenticationServiceSetupParams { - legacyAuditLogger: SecurityAuditLogger; - audit: AuditServiceSetup; - getFeatureUsageService: () => SecurityFeatureUsageServiceStart; - http: HttpServiceSetup; - clusterClient: ILegacyClusterClient; - config: ConfigType; + http: Pick; license: SecurityLicense; - loggers: LoggerFactory; - session: PublicMethodsOf; } interface AuthenticationServiceStartParams { - http: HttpServiceStart; + http: Pick; + config: ConfigType; clusterClient: IClusterClient; -} - -export interface AuthenticationServiceSetup { - /** - * @deprecated use `getCurrentUser` from the start contract instead - */ - getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; + featureUsageService: SecurityFeatureUsageServiceStart; + session: PublicMethodsOf; + loggers: LoggerFactory; } export interface AuthenticationServiceStart { @@ -67,44 +58,13 @@ export interface AuthenticationServiceStart { export class AuthenticationService { private license!: SecurityLicense; - private authenticator!: Authenticator; + private authenticator?: Authenticator; constructor(private readonly logger: Logger) {} - setup({ - legacyAuditLogger: auditLogger, - audit, - getFeatureUsageService, - http, - clusterClient, - config, - license, - loggers, - session, - }: AuthenticationServiceSetupParams): AuthenticationServiceSetup { + setup({ http, license }: AuthenticationServiceSetupParams) { this.license = license; - const getCurrentUser = (request: KibanaRequest) => { - if (!license.isEnabled()) { - return null; - } - - return http.auth.get(request).state ?? null; - }; - - this.authenticator = new Authenticator({ - legacyAuditLogger: auditLogger, - audit, - loggers, - clusterClient, - basePath: http.basePath, - config: { authc: config.authc }, - getCurrentUser, - getFeatureUsageService, - license, - session, - }); - http.registerAuth(async (request, response, t) => { if (!license.isLicenseAvailable()) { this.logger.error('License is not available, authentication is not possible.'); @@ -123,6 +83,15 @@ export class AuthenticationService { return t.authenticated(); } + if (!this.authenticator) { + this.logger.error('Authentication sub-system is not fully initialized yet.'); + return response.customError({ + body: 'Authentication sub-system is not fully initialized yet.', + statusCode: 503, + headers: { 'Retry-After': '30' }, + }); + } + let authenticationResult; try { authenticationResult = await this.authenticator.authenticate(request); @@ -174,19 +143,40 @@ export class AuthenticationService { }); this.logger.debug('Successfully registered core authentication handler.'); - - return { - getCurrentUser, - }; } - start({ clusterClient, http }: AuthenticationServiceStartParams): AuthenticationServiceStart { + start({ + audit, + config, + clusterClient, + featureUsageService, + http, + legacyAuditLogger, + loggers, + session, + }: AuthenticationServiceStartParams): AuthenticationServiceStart { const apiKeys = new APIKeys({ clusterClient, logger: this.logger.get('api-key'), license: this.license, }); + const getCurrentUser = (request: KibanaRequest) => + http.auth.get(request).state ?? null; + + this.authenticator = new Authenticator({ + legacyAuditLogger, + audit, + loggers, + clusterClient, + basePath: http.basePath, + config: { authc: config.authc }, + getCurrentUser, + featureUsageService, + license: this.license, + session, + }); + return { apiKeys: { areAPIKeysEnabled: apiKeys.areAPIKeysEnabled.bind(apiKeys), @@ -206,12 +196,7 @@ export class AuthenticationService { * Retrieves currently authenticated user associated with the specified request. * @param request */ - getCurrentUser: (request: KibanaRequest) => { - if (!this.license.isEnabled()) { - return null; - } - return http.auth.get(request).state ?? null; - }, + getCurrentUser, }; } } diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 3d3946fde9f34..08d671d64179a 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -43,7 +43,7 @@ function getMockOptions({ legacyAuditLogger: securityAuditLoggerMock.create(), audit: auditServiceMock.create(), getCurrentUser: jest.fn(), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), + clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, license: licenseMock.create(), loggers: loggingSystemMock.create(), @@ -53,9 +53,7 @@ function getMockOptions({ { isTLSEnabled: false } ), session: sessionMock.create(), - getFeatureUsageService: jest - .fn() - .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), + featureUsageService: securityFeatureUsageServiceMock.createStartContract(), }; } @@ -1880,9 +1878,7 @@ describe('Authenticator', () => { ); expect(mockOptions.session.update).not.toHaveBeenCalled(); - expect( - mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage - ).not.toHaveBeenCalled(); + expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); }); it('fails if cannot retrieve user session', async () => { @@ -1895,12 +1891,10 @@ describe('Authenticator', () => { ); expect(mockOptions.session.update).not.toHaveBeenCalled(); - expect( - mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage - ).not.toHaveBeenCalled(); + expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); }); - it('fails if license doesn allow access agreement acknowledgement', async () => { + it('fails if license does not allow access agreement acknowledgement', async () => { mockOptions.license.getFeatures.mockReturnValue({ allowAccessAgreement: false, } as SecurityLicenseFeatures); @@ -1912,9 +1906,7 @@ describe('Authenticator', () => { ); expect(mockOptions.session.update).not.toHaveBeenCalled(); - expect( - mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage - ).not.toHaveBeenCalled(); + expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); }); it('properly acknowledges access agreement for the authenticated user', async () => { @@ -1936,9 +1928,9 @@ describe('Authenticator', () => { } ); - expect( - mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage - ).toHaveBeenCalledTimes(1); + expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).toHaveBeenCalledTimes( + 1 + ); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 85215ebf46fb4..af492bf247726 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -7,8 +7,8 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaRequest, LoggerFactory, - ILegacyClusterClient, IBasePath, + IClusterClient, } from '../../../../../src/core/server'; import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, @@ -68,13 +68,13 @@ export interface ProviderLoginAttempt { export interface AuthenticatorOptions { legacyAuditLogger: SecurityAuditLogger; audit: AuditServiceSetup; - getFeatureUsageService: () => SecurityFeatureUsageServiceStart; + featureUsageService: SecurityFeatureUsageServiceStart; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick; basePath: IBasePath; license: SecurityLicense; loggers: LoggerFactory; - clusterClient: ILegacyClusterClient; + clusterClient: IClusterClient; session: PublicMethodsOf; } @@ -201,7 +201,7 @@ export class Authenticator { client: this.options.clusterClient, basePath: this.options.basePath, tokens: new Tokens({ - client: this.options.clusterClient, + client: this.options.clusterClient.asInternalUser, logger: this.options.loggers.get('tokens'), }), }; @@ -448,7 +448,7 @@ export class Authenticator { existingSessionValue.provider ); - this.options.getFeatureUsageService().recordPreAccessAgreementUsage(); + this.options.featureUsageService.recordPreAccessAgreementUsage(); } /** diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index c87a02c9545c1..e745e1c5717b3 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -5,11 +5,7 @@ */ export { canRedirectRequest } from './can_redirect_request'; -export { - AuthenticationService, - AuthenticationServiceSetup, - AuthenticationServiceStart, -} from './authentication_service'; +export { AuthenticationService, AuthenticationServiceStart } from './authentication_service'; export { AuthenticationResult } from './authentication_result'; export { DeauthenticationResult } from './deauthentication_result'; export { diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts index 9674181e18750..a2db413319546 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors } from '@elastic/elasticsearch'; + import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { mockAuthenticationProviderOptions } from './base.mock'; -import { ILegacyClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { @@ -18,15 +21,14 @@ import { import { AnonymousAuthenticationProvider } from './anonymous'; function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } enum CredentialsType { @@ -75,8 +77,10 @@ describe('AnonymousAuthenticationProvider', () => { describe('`login` method', () => { it('succeeds if credentials are valid, and creates session and authHeaders', async () => { - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect( @@ -92,10 +96,13 @@ describe('AnonymousAuthenticationProvider', () => { it('fails if user cannot be retrieved during login attempt', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - - const authenticationError = new Error('Some error'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + authenticationError + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.login(request)).resolves.toEqual( @@ -155,8 +162,10 @@ describe('AnonymousAuthenticationProvider', () => { it('succeeds for non-AJAX requests if state is available.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, {})).resolves.toEqual( @@ -169,8 +178,10 @@ describe('AnonymousAuthenticationProvider', () => { it('succeeds for AJAX requests if state is available.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, {})).resolves.toEqual( @@ -185,8 +196,10 @@ describe('AnonymousAuthenticationProvider', () => { it('non-AJAX requests can start a new session.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request)).resolves.toEqual( @@ -199,9 +212,13 @@ describe('AnonymousAuthenticationProvider', () => { it('fails if credentials are not valid.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const authenticationError = new Error('Forbidden'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + authenticationError + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request)).resolves.toEqual( @@ -225,8 +242,10 @@ describe('AnonymousAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, {})).resolves.toEqual( diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts index 1585b0592b356..249b4adea7bba 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, LegacyElasticsearchErrorHelpers } from '../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { getErrorStatusCode } from '../../errors'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -213,7 +214,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider // Create session only if it doesn't exist yet, otherwise keep it unchanged. return AuthenticationResult.succeeded(user, { authHeaders, state: state ? undefined : {} }); } catch (err) { - if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(err)) { + if (getErrorStatusCode(err) === 401) { if (!this.httpAuthorizationHeader) { this.logger.error( `Failed to authenticate anonymous request using Elasticsearch reserved anonymous user. Anonymous access may not be properly configured in Elasticsearch: ${err.message}` diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 47d961bc8faf8..3eea6f9aadadf 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -16,7 +16,7 @@ export type MockAuthenticationProviderOptions = ReturnType< export function mockAuthenticationProviderOptions(options?: { name: string }) { return { - client: elasticsearchServiceMock.createLegacyClusterClient(), + client: elasticsearchServiceMock.createClusterClient(), logger: loggingSystemMock.create().get(), basePath: httpServiceMock.createBasePath(), tokens: { refresh: jest.fn(), invalidate: jest.fn() }, diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index f1845617c87a4..73449cf1077fe 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -10,8 +10,8 @@ import { KibanaRequest, Logger, HttpServiceSetup, - ILegacyClusterClient, Headers, + IClusterClient, } from '../../../../../../src/core/server'; import type { AuthenticatedUser } from '../../../common/model'; import type { AuthenticationInfo } from '../../elasticsearch'; @@ -25,7 +25,7 @@ import { Tokens } from '../tokens'; export interface AuthenticationProviderOptions { name: string; basePath: HttpServiceSetup['basePath']; - client: ILegacyClusterClient; + client: IClusterClient; logger: Logger; tokens: PublicMethodsOf; urls: { @@ -111,9 +111,11 @@ export abstract class BaseAuthenticationProvider { */ protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) { return this.authenticationInfoToAuthenticatedUser( - await this.options.client - .asScoped({ headers: { ...request.headers, ...authHeaders } }) - .callAsCurrentUser('shield.authenticate') + ( + await this.options.client + .asScoped({ headers: { ...request.headers, ...authHeaders } }) + .asCurrentUser.security.authenticate() + ).body ); } diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 4f93e2327da06..e7cf3d95b0827 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors } from '@elastic/elasticsearch'; + import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { mockAuthenticationProviderOptions } from './base.mock'; -import { ILegacyClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { BasicAuthenticationProvider } from './basic'; @@ -18,15 +21,14 @@ function generateAuthorizationHeader(username: string, password: string) { } function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } describe('BasicAuthenticationProvider', () => { @@ -45,8 +47,10 @@ describe('BasicAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect( @@ -66,9 +70,13 @@ describe('BasicAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); - const authenticationError = new Error('Some error'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + authenticationError + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.login(request, credentials)).resolves.toEqual( @@ -149,8 +157,10 @@ describe('BasicAuthenticationProvider', () => { const user = mockAuthenticatedUser(); const authorization = generateAuthorizationHeader('user', 'password'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, { authorization })).resolves.toEqual( @@ -164,9 +174,13 @@ describe('BasicAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = generateAuthorizationHeader('user', 'password'); - const authenticationError = new Error('Forbidden'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + authenticationError + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, { authorization })).resolves.toEqual( diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts index 512a8ead2c32b..b8a2a110d45b0 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -4,29 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors } from '@elastic/elasticsearch'; + import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - ILegacyClusterClient, - ScopeableRequest, -} from '../../../../../../src/core/server'; +import { ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { HTTPAuthenticationProvider } from './http'; function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } describe('HTTPAuthenticationProvider', () => { @@ -58,7 +56,6 @@ describe('HTTPAuthenticationProvider', () => { await expect(provider.login()).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); }); @@ -73,7 +70,6 @@ describe('HTTPAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('does not handle authentication for requests with empty scheme in `authorization` header.', async () => { @@ -88,7 +84,6 @@ describe('HTTPAuthenticationProvider', () => { ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('does not handle authentication via `authorization` header if scheme is not supported.', async () => { @@ -112,7 +107,6 @@ describe('HTTPAuthenticationProvider', () => { } expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('succeeds if authentication via `authorization` header with supported scheme succeeds.', async () => { @@ -126,8 +120,10 @@ describe('HTTPAuthenticationProvider', () => { ]) { const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.asScoped.mockClear(); @@ -149,7 +145,7 @@ describe('HTTPAuthenticationProvider', () => { }); it('fails if authentication via `authorization` header with supported scheme fails.', async () => { - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + const failureReason = new errors.ResponseError(securityMock.createApiResponse({ body: {} })); for (const { schemes, header } of [ { schemes: ['basic'], header: 'Basic xxx' }, { schemes: ['bearer'], header: 'Bearer xxx' }, @@ -159,8 +155,10 @@ describe('HTTPAuthenticationProvider', () => { ]) { const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + failureReason + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.asScoped.mockClear(); @@ -188,7 +186,6 @@ describe('HTTPAuthenticationProvider', () => { await expect(provider.logout()).resolves.toEqual(DeauthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index d368bf90cf360..f8b7b42d1845b 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -5,32 +5,27 @@ */ import Boom from '@hapi/boom'; -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - ILegacyClusterClient, - KibanaRequest, - ScopeableRequest, -} from '../../../../../../src/core/server'; +import { KibanaRequest, ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { KerberosAuthenticationProvider } from './kerberos'; function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } describe('KerberosAuthenticationProvider', () => { @@ -47,8 +42,10 @@ describe('KerberosAuthenticationProvider', () => { it('does not handle requests that can be authenticated without `Negotiate` header.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({}); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: {} }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); @@ -61,9 +58,9 @@ describe('KerberosAuthenticationProvider', () => { it('does not handle requests if backend does not support Kerberos.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -77,17 +74,18 @@ describe('KerberosAuthenticationProvider', () => { it('fails with `Negotiate` challenge if backend supports Kerberos.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 401, body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(operation(request)).resolves.toEqual( - AuthenticationResult.failed(failureReason, { + AuthenticationResult.failed(Boom.unauthorized(), { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); @@ -100,9 +98,12 @@ describe('KerberosAuthenticationProvider', () => { it('fails if request authentication is failed with non-401 error.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const failureReason = new errors.ServiceUnavailable(); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const failureReason = new errors.NoLivingConnectionsError( + 'Unavailable', + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(operation(request)).resolves.toEqual(AuthenticationResult.failed(failureReason)); @@ -118,11 +119,15 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'some-token', - refresh_token: 'some-refresh-token', - authentication: user, - }); + mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'some-token', + refresh_token: 'some-refresh-token', + authentication: user, + }, + }) + ); await expect(operation(request)).resolves.toEqual( AuthenticationResult.succeeded( @@ -135,7 +140,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); @@ -148,12 +153,16 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'some-token', - refresh_token: 'some-refresh-token', - kerberos_authentication_response_token: 'response-token', - authentication: user, - }); + mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'some-token', + refresh_token: 'some-refresh-token', + kerberos_authentication_response_token: 'response-token', + authentication: user, + }, + }) + ); await expect(operation(request)).resolves.toEqual( AuthenticationResult.succeeded( @@ -167,7 +176,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); @@ -179,12 +188,13 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 401, body: { error: { header: { 'WWW-Authenticate': 'Negotiate response-token' } } }, }) ); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + mockOptions.client.asInternalUser.security.getToken.mockRejectedValue(failureReason); await expect(operation(request)).resolves.toEqual( AuthenticationResult.failed(Boom.unauthorized(), { @@ -192,7 +202,7 @@ describe('KerberosAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); @@ -204,12 +214,13 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 401, body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + mockOptions.client.asInternalUser.security.getToken.mockRejectedValue(failureReason); await expect(operation(request)).resolves.toEqual( AuthenticationResult.failed(Boom.unauthorized(), { @@ -217,7 +228,7 @@ describe('KerberosAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); @@ -229,12 +240,14 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); + mockOptions.client.asInternalUser.security.getToken.mockRejectedValue(failureReason); await expect(operation(request)).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); @@ -259,7 +272,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.security.getToken).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); }); @@ -277,7 +290,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.security.getToken).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); }); @@ -285,14 +298,15 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'token', refreshToken: 'refresh-token' }; - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.tokens.refresh.mockResolvedValue(null); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( - AuthenticationResult.failed(failureReason) + AuthenticationResult.failed(Boom.unauthorized()) ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); @@ -306,7 +320,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.security.getToken).not.toHaveBeenCalled(); }); it('does not start SPNEGO for Ajax requests.', async () => { @@ -316,7 +330,7 @@ describe('KerberosAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.security.getToken).not.toHaveBeenCalled(); }); it('succeeds if state contains a valid token.', async () => { @@ -328,8 +342,10 @@ describe('KerberosAuthenticationProvider', () => { }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( @@ -349,9 +365,9 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -384,9 +400,11 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'some-valid-refresh-token', }; - const failureReason = new errors.InternalServerError('Token is not valid!'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( @@ -396,23 +414,22 @@ describe('KerberosAuthenticationProvider', () => { expectAuthenticateCall(mockOptions.client, { headers: { authorization: `Bearer ${tokenPair.accessToken}` }, }); - - expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.security.getToken).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); }); it('fails with `Negotiate` challenge if both access and refresh tokens from the state are expired and backend supports Kerberos.', async () => { - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 401, + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, + }) + ) ); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.mockResolvedValue(null); const nonAjaxRequest = httpServerMock.createKibanaRequest(); @@ -421,7 +438,7 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'some-valid-refresh-token', }; await expect(provider.authenticate(nonAjaxRequest, nonAjaxTokenPair)).resolves.toEqual( - AuthenticationResult.failed(failureReason, { + AuthenticationResult.failed(Boom.unauthorized(), { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); @@ -432,7 +449,7 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'ajax-some-valid-refresh-token', }; await expect(provider.authenticate(ajaxRequest, ajaxTokenPair)).resolves.toEqual( - AuthenticationResult.failed(failureReason, { + AuthenticationResult.failed(Boom.unauthorized(), { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); @@ -445,7 +462,7 @@ describe('KerberosAuthenticationProvider', () => { await expect( provider.authenticate(optionalAuthRequest, optionalAuthTokenPair) ).resolves.toEqual( - AuthenticationResult.failed(failureReason, { + AuthenticationResult.failed(Boom.unauthorized(), { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index b7abed979164e..a02f6a8dfb945 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -5,12 +5,10 @@ */ import Boom from '@hapi/boom'; -import { - LegacyElasticsearchError, - LegacyElasticsearchErrorHelpers, - KibanaRequest, -} from '../../../../../../src/core/server'; +import { errors } from '@elastic/elasticsearch'; +import type { KibanaRequest } from '../../../../../../src/core/server'; import type { AuthenticationInfo } from '../../elasticsearch'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../../errors'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { HTTPAuthorizationHeader } from '../http_authentication'; @@ -153,16 +151,21 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { authentication: AuthenticationInfo; }; try { - tokens = await this.options.client.callAsInternalUser('shield.getAccessToken', { - body: { grant_type: '_kerberos', kerberos_ticket: kerberosTicket }, - }); + tokens = ( + await this.options.client.asInternalUser.security.getToken({ + body: { grant_type: '_kerberos', kerberos_ticket: kerberosTicket }, + }) + ).body; } catch (err) { - this.logger.debug(`Failed to exchange SPNEGO token for an access token: ${err.message}`); + this.logger.debug( + `Failed to exchange SPNEGO token for an access token: ${getDetailedErrorMessage(err)}` + ); // Check if SPNEGO context wasn't established and we have a response token to return to the client. - const challenge = LegacyElasticsearchErrorHelpers.isNotAuthorizedError(err) - ? this.getNegotiateChallenge(err) - : undefined; + const challenge = + getErrorStatusCode(err) === 401 && err instanceof errors.ResponseError + ? this.getNegotiateChallenge(err) + : undefined; if (!challenge) { return AuthenticationResult.failed(err); } @@ -292,7 +295,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Trying to authenticate request via SPNEGO.'); // Try to authenticate current request with Elasticsearch to see whether it supports SPNEGO. - let elasticsearchError: LegacyElasticsearchError; + let elasticsearchError: errors.ResponseError; try { await this.getUser(request, { // We should send a fake SPNEGO token to Elasticsearch to make sure Kerberos realm is included @@ -306,7 +309,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { } catch (err) { // Fail immediately if we get unexpected error (e.g. ES isn't available). We should not touch // session cookie in this case. - if (!LegacyElasticsearchErrorHelpers.isNotAuthorizedError(err)) { + if (getErrorStatusCode(err) !== 401 || !(err instanceof errors.ResponseError)) { return AuthenticationResult.failed(err); } @@ -332,11 +335,14 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { * Extracts `Negotiate` challenge from the list of challenges returned with Elasticsearch error if any. * @param error Error to extract challenges from. */ - private getNegotiateChallenge(error: LegacyElasticsearchError) { + private getNegotiateChallenge(error: errors.ResponseError) { + // We extract headers from the original Elasticsearch error and not from the top-level `headers` + // property of the Elasticsearch client error since client merges multiple `WWW-Authenticate` + // headers into one using comma as a separator. That makes it hard to correctly parse the header + // since `WWW-Authenticate` values can also include commas. const challenges = ([] as string[]).concat( - (error.output.headers as { [key: string]: string })[WWWAuthenticateHeaderName] + error.body?.error?.header?.[WWWAuthenticateHeaderName] || [] ); - const negotiateChallenge = challenges.find((challenge) => challenge.toLowerCase().startsWith('negotiate') ); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index 9988ddd99c395..8037b067852d8 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -5,16 +5,14 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - KibanaRequest, - ILegacyScopedClusterClient, -} from '../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { OIDCAuthenticationProvider, OIDCLogin, ProviderLoginAttempt } from './oidc'; @@ -24,19 +22,18 @@ describe('OIDCAuthenticationProvider', () => { let provider: OIDCAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; let mockUser: AuthenticatedUser; - let mockScopedClusterClient: jest.Mocked; + let mockScopedClusterClient: ReturnType< + typeof elasticsearchServiceMock.createScopedClusterClient + >; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'oidc' }); - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockUser = mockAuthenticatedUser({ authentication_provider: { type: 'oidc', name: 'oidc' } }); - mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method) => { - if (method === 'shield.authenticate') { - return mockUser; - } - - throw new Error(`Unexpected call to ${method}!`); - }); + mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: mockUser }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); provider = new OIDCAuthenticationProvider(mockOptions, { realm: 'oidc1' }); @@ -60,17 +57,21 @@ describe('OIDCAuthenticationProvider', () => { it('redirects third party initiated login attempts to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/security/oidc/callback' }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - state: 'statevalue', - nonce: 'noncevalue', - redirect: - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + - '&login_hint=loginhint', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + state: 'statevalue', + nonce: 'noncevalue', + redirect: + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + + '&login_hint=loginhint', + }, + }) + ); await expect( provider.login(request, { @@ -97,7 +98,10 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/prepare', body: { iss: 'theissuer', login_hint: 'loginhint' }, }); }); @@ -105,17 +109,21 @@ describe('OIDCAuthenticationProvider', () => { it('redirects user initiated login attempts to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - state: 'statevalue', - nonce: 'noncevalue', - redirect: - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + - '&login_hint=loginhint', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + state: 'statevalue', + nonce: 'noncevalue', + redirect: + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + + '&login_hint=loginhint', + }, + }) + ); await expect( provider.login(request, { @@ -141,7 +149,10 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/prepare', body: { realm: 'oidc1' }, }); }); @@ -149,8 +160,10 @@ describe('OIDCAuthenticationProvider', () => { it('fails if OpenID Connect authentication request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest(); - const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login(request, { @@ -159,8 +172,11 @@ describe('OIDCAuthenticationProvider', () => { }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { - body: { realm: `oidc1` }, + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/prepare', + body: { realm: 'oidc1' }, }); }); @@ -174,11 +190,15 @@ describe('OIDCAuthenticationProvider', () => { it('gets token and redirects user to requested URL if OIDC authentication response is valid.', async () => { const { request, attempt, expectedRedirectURI } = getMocks(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: mockUser, - access_token: 'some-token', - refresh_token: 'some-refresh-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + authentication: mockUser, + access_token: 'some-token', + refresh_token: 'some-refresh-token', + }, + }) + ); await expect( provider.login(request, attempt, { @@ -198,17 +218,17 @@ describe('OIDCAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.oidcAuthenticate', - { - body: { - state: 'statevalue', - nonce: 'noncevalue', - redirect_uri: expectedRedirectURI, - realm: 'oidc1', - }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/authenticate', + body: { + state: 'statevalue', + nonce: 'noncevalue', + redirect_uri: expectedRedirectURI, + realm: 'oidc1', + }, + }); }); it('fails if authentication response is presented but session state does not contain the state parameter.', async () => { @@ -224,7 +244,7 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if authentication response is presented but session state does not contain redirect URL.', async () => { @@ -244,7 +264,7 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if session state is not presented.', async () => { @@ -258,16 +278,19 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if authentication response is not valid.', async () => { const { request, attempt, expectedRedirectURI } = getMocks(); - const failureReason = new Error( - 'Failed to exchange code for Id Token using the Token Endpoint.' + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 400, + body: { message: 'Failed to exchange code for Id Token using the Token Endpoint.' }, + }) ); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login(request, attempt, { @@ -278,17 +301,17 @@ describe('OIDCAuthenticationProvider', () => { }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.oidcAuthenticate', - { - body: { - state: 'statevalue', - nonce: 'noncevalue', - redirect_uri: expectedRedirectURI, - realm: 'oidc1', - }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/authenticate', + body: { + state: 'statevalue', + nonce: 'noncevalue', + redirect_uri: expectedRedirectURI, + realm: 'oidc1', + }, + }); }); it('fails if realm from state is different from the realm provider is configured with.', async () => { @@ -302,7 +325,7 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); } @@ -353,11 +376,6 @@ describe('OIDCAuthenticationProvider', () => { it('redirects non-AJAX request that can not be authenticated to the "capture URL" page.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - id: 'some-request-id', - redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', - }); - await expect(provider.authenticate(request, null)).resolves.toEqual( AuthenticationResult.redirectTo( '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=oidc&providerName=oidc', @@ -365,7 +383,7 @@ describe('OIDCAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('succeeds if state contains a valid token.', async () => { @@ -425,8 +443,10 @@ describe('OIDCAuthenticationProvider', () => { }; const authorization = `Bearer ${tokenPair.accessToken}`; - const failureReason = new Error('Token is not valid!'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 400, body: {} }) + ); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); await expect( provider.authenticate(request, { ...tokenPair, realm: 'oidc1' }) @@ -441,8 +461,8 @@ describe('OIDCAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' }; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue({ @@ -475,8 +495,8 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'invalid-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); const refreshFailureReason = { @@ -502,8 +522,8 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -524,10 +544,9 @@ describe('OIDCAuthenticationProvider', () => { expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { @@ -535,8 +554,8 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -562,8 +581,8 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -604,7 +623,7 @@ describe('OIDCAuthenticationProvider', () => { await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects to logged out view if state is `null` or does not include access token.', async () => { @@ -617,7 +636,7 @@ describe('OIDCAuthenticationProvider', () => { DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if OpenID Connect logout call fails.', async () => { @@ -625,15 +644,22 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 400, + body: { message: 'Realm is misconfigured!' }, + }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' }) ).resolves.toEqual(DeauthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -643,14 +669,18 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: null } }) + ); await expect( provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' }) ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -660,9 +690,11 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ - redirect: 'http://fake-idp/logout&id_token_hint=thehint', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { redirect: 'http://fake-idp/logout&id_token_hint=thehint' }, + }) + ); await expect( provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' }) @@ -670,8 +702,10 @@ describe('OIDCAuthenticationProvider', () => { DeauthenticationResult.redirectTo('http://fake-idp/logout&id_token_hint=thehint') ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/oidc/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index c46ea37f144e9..b89267f44eeeb 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -248,14 +248,20 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/oidc/authenticate`. - result = await this.options.client.callAsInternalUser('shield.oidcAuthenticate', { - body: { - state: stateOIDCState, - nonce: stateNonce, - redirect_uri: authenticationResponseURI, - realm: this.realm, - }, - }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + result = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/oidc/authenticate', + body: { + state: stateOIDCState, + nonce: stateNonce, + redirect_uri: authenticationResponseURI, + realm: this.realm, + }, + }) + ).body as any; } catch (err) { this.logger.debug(`Failed to authenticate request via OpenID Connect: ${err.message}`); return AuthenticationResult.failed(err); @@ -289,11 +295,15 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/oidc/prepare`. - const { - state, - nonce, - redirect, - } = await this.options.client.callAsInternalUser('shield.oidcPrepare', { body: params }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + const { state, nonce, redirect } = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/oidc/prepare', + body: params, + }) + ).body as any; this.logger.debug('Redirecting to OpenID Connect Provider with authentication request.'); return AuthenticationResult.redirectTo( @@ -407,18 +417,17 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { if (state?.accessToken) { try { - const logoutBody = { - body: { - token: state.accessToken, - refresh_token: state.refreshToken, - }, - }; // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/oidc/logout`. - const { redirect } = await this.options.client.callAsInternalUser( - 'shield.oidcLogout', - logoutBody - ); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + const { redirect } = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/oidc/logout', + body: { token: state.accessToken, refresh_token: state.refreshToken }, + }) + ).body as any; this.logger.debug('User session has been successfully invalidated.'); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 88753f8dc2ab1..d98d6ca4fa071 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -10,18 +10,14 @@ jest.mock('tls'); import { Socket } from 'net'; import { PeerCertificate, TLSSocket } from 'tls'; import Boom from '@hapi/boom'; -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - ILegacyClusterClient, - KibanaRequest, - ScopeableRequest, -} from '../../../../../../src/core/server'; +import { KibanaRequest, ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { PKIAuthenticationProvider } from './pki'; @@ -87,15 +83,14 @@ function getMockSocket({ } function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } describe('PKIAuthenticationProvider', () => { @@ -125,7 +120,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', 'Authentication is not possible since peer certificate was not authorized: Error: mock authorization error.' @@ -139,7 +134,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( 'Peer certificate chain: []', 'Authentication is not possible due to missing peer certificate chain.' @@ -159,7 +154,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( `Detected incomplete certificate chain with protocol 'TLSv1.3', cannot renegotiate connection.`, 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', @@ -181,7 +176,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, `Failed to renegotiate connection: Error: Oh no!.`, @@ -203,7 +198,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', @@ -231,7 +226,7 @@ describe('PKIAuthenticationProvider', () => { await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expectDebugLogs( `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, 'Self-signed certificate is detected in certificate chain', @@ -253,10 +248,11 @@ describe('PKIAuthenticationProvider', () => { mockGetPeerCertificate.mockReturnValueOnce(peerCertificate1); const request = httpServerMock.createKibanaRequest({ socket }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: user, - access_token: 'access-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { authentication: user, access_token: 'access-token' }, + }) + ); await expect(operation(request)).resolves.toEqual( AuthenticationResult.succeeded( @@ -268,8 +264,10 @@ describe('PKIAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:2A:7A:C2:DD:base64', @@ -295,10 +293,11 @@ describe('PKIAuthenticationProvider', () => { const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: user, - access_token: 'access-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { authentication: user, access_token: 'access-token' }, + }) + ); await expect(operation(request)).resolves.toEqual( AuthenticationResult.succeeded( @@ -310,8 +309,10 @@ describe('PKIAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:2A:7A:C2:DD:base64', @@ -330,10 +331,11 @@ describe('PKIAuthenticationProvider', () => { const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: user, - access_token: 'access-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { authentication: user, access_token: 'access-token' }, + }) + ); await expect(operation(request)).resolves.toEqual( AuthenticationResult.succeeded( @@ -345,8 +347,10 @@ describe('PKIAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); @@ -359,13 +363,17 @@ describe('PKIAuthenticationProvider', () => { const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); - const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect(operation(request)).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); @@ -390,7 +398,7 @@ describe('PKIAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); }); @@ -408,7 +416,7 @@ describe('PKIAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); }); @@ -421,7 +429,7 @@ describe('PKIAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('does not exchange peer certificate to access token for Ajax requests.', async () => { @@ -436,7 +444,7 @@ describe('PKIAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails with non-401 error if state is available, peer is authorized, but certificate is not available.', async () => { @@ -490,10 +498,11 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '3A:9A:C5:DD' }; - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: user, - access_token: 'access-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { authentication: user, access_token: 'access-token' }, + }) + ); await expect(provider.authenticate(request, state)).resolves.toEqual( AuthenticationResult.succeeded( @@ -510,8 +519,10 @@ describe('PKIAuthenticationProvider', () => { accessToken: state.accessToken, }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:2A:7A:C2:DD:base64', @@ -526,15 +537,16 @@ describe('PKIAuthenticationProvider', () => { it('gets a new access token even if existing token is expired.', async () => { const user = mockAuthenticatedUser({ authentication_provider: { type: 'pki', name: 'pki' } }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - authentication: user, - access_token: 'access-token', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { authentication: user, access_token: 'access-token' }, + }) + ); const nonAjaxRequest = httpServerMock.createKibanaRequest({ socket: getMockSocket({ @@ -589,8 +601,10 @@ describe('PKIAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(3); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(3); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:2A:7A:C2:DD:base64', @@ -598,7 +612,9 @@ describe('PKIAuthenticationProvider', () => { ], }, }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:3A:7A:C2:DD:base64', @@ -606,7 +622,9 @@ describe('PKIAuthenticationProvider', () => { ], }, }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/delegate_pki', body: { x509_certificate_chain: [ 'fingerprint:4A:7A:C2:DD:base64', @@ -625,9 +643,9 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -635,7 +653,7 @@ describe('PKIAuthenticationProvider', () => { AuthenticationResult.failed(Boom.unauthorized()) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -647,8 +665,10 @@ describe('PKIAuthenticationProvider', () => { const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, state)).resolves.toEqual( @@ -660,7 +680,7 @@ describe('PKIAuthenticationProvider', () => { expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -671,9 +691,12 @@ describe('PKIAuthenticationProvider', () => { const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); - const failureReason = new errors.ServiceUnavailable(); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const failureReason = new errors.ConnectionError( + 'unavailable', + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, state)).resolves.toEqual( diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 3171c5ff24abe..a5dc870bf9c84 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -272,9 +272,15 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { let result: { access_token: string; authentication: AuthenticationInfo }; try { - result = await this.options.client.callAsInternalUser('shield.delegatePKI', { - body: { x509_certificate_chain: certificateChain }, - }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + result = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/delegate_pki', + body: { x509_certificate_chain: certificateChain }, + }) + ).body as any; } catch (err) { this.logger.debug( `Failed to exchange peer certificate chain to an access token: ${err.message}` @@ -298,7 +304,8 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Obtains the peer certificate chain. Starts from the leaf peer certificate and iterates up to the top-most available certificate + * Obtains the peer certificate chain as an ordered array of base64-encoded (Section 4 of RFC4648 - not base64url-encoded) + * DER PKIX certificate values. Starts from the leaf peer certificate and iterates up to the top-most available certificate * authority using `issuerCertificate` certificate property. THe iteration is stopped only when we detect circular reference * (root/self-signed certificate) or when `issuerCertificate` isn't available (null or empty object). Automatically attempts to * renegotiate the TLS connection once if the peer certificate chain is incomplete. diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 5cba017e4916b..48334358bb001 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -5,15 +5,13 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - ILegacyScopedClusterClient, -} from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { SAMLAuthenticationProvider, SAMLLogin } from './saml'; @@ -23,19 +21,17 @@ describe('SAMLAuthenticationProvider', () => { let provider: SAMLAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; let mockUser: AuthenticatedUser; - let mockScopedClusterClient: jest.Mocked; + let mockScopedClusterClient: ReturnType< + typeof elasticsearchServiceMock.createScopedClusterClient + >; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'saml' }); - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockUser = mockAuthenticatedUser({ authentication_provider: { type: 'saml', name: 'saml' } }); - mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method) => { - if (method === 'shield.authenticate') { - return mockUser; - } - - throw new Error(`Unexpected call to ${method}!`); - }); + mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: mockUser }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); provider = new SAMLAuthenticationProvider(mockOptions, { @@ -61,11 +57,15 @@ describe('SAMLAuthenticationProvider', () => { it('gets token and redirects user to requested URL if SAML Response is valid.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'some-token', - refresh_token: 'some-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'some-token', + refresh_token: 'some-refresh-token', + authentication: mockUser, + }, + }) + ); await expect( provider.login( @@ -88,20 +88,25 @@ describe('SAMLAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('gets token and redirects user to the requested URL if SAML Response is valid ignoring Relay State.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'some-token', - refresh_token: 'some-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'some-token', + refresh_token: 'some-refresh-token', + authentication: mockUser, + }, + }) + ); provider = new SAMLAuthenticationProvider(mockOptions, { realm: 'test-realm', @@ -132,10 +137,11 @@ describe('SAMLAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('fails if SAML Response payload is presented but state does not contain SAML Request token.', async () => { @@ -153,7 +159,7 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if realm from state is different from the realm provider is configured with.', async () => { @@ -173,17 +179,21 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects to the default location if state contains empty redirect URL.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'user-initiated-login-token', - refresh_token: 'user-initiated-login-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'user-initiated-login-token', + refresh_token: 'user-initiated-login-refresh-token', + authentication: mockUser, + }, + }) + ); await expect( provider.login( @@ -202,20 +212,25 @@ describe('SAMLAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('redirects to the default location if state contains empty redirect URL ignoring Relay State.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'user-initiated-login-token', - refresh_token: 'user-initiated-login-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'user-initiated-login-token', + refresh_token: 'user-initiated-login-refresh-token', + authentication: mockUser, + }, + }) + ); provider = new SAMLAuthenticationProvider(mockOptions, { realm: 'test-realm', @@ -242,20 +257,25 @@ describe('SAMLAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('redirects to the default location if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: 'idp-initiated-login-token', - refresh_token: 'idp-initiated-login-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: 'idp-initiated-login-token', + refresh_token: 'idp-initiated-login-refresh-token', + authentication: mockUser, + }, + }) + ); await expect( provider.login(request, { @@ -273,17 +293,20 @@ describe('SAMLAuthenticationProvider', () => { }) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('fails if SAML Response is rejected.', async () => { const request = httpServerMock.createKibanaRequest(); - const failureReason = new Error('SAML response is stale!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login( @@ -297,22 +320,27 @@ describe('SAMLAuthenticationProvider', () => { ) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); describe('IdP initiated login', () => { beforeEach(() => { mockOptions.basePath.get.mockReturnValue(mockOptions.basePath.serverBasePath); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'valid-token', - refresh_token: 'valid-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + username: 'user', + access_token: 'valid-token', + refresh_token: 'valid-refresh-token', + authentication: mockUser, + }, + }) + ); provider = new SAMLAuthenticationProvider(mockOptions, { realm: 'test-realm', @@ -428,8 +456,10 @@ describe('SAMLAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = 'Bearer some-valid-token'; - const failureReason = new Error('SAML response is invalid!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login( @@ -444,12 +474,11 @@ describe('SAMLAuthenticationProvider', () => { ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); }); it('fails if fails to invalidate existing access/refresh tokens.', async () => { @@ -461,12 +490,16 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - authentication: mockUser, - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + authentication: mockUser, + }, + }) + ); const failureReason = new Error('Failed to invalidate token!'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); @@ -480,12 +513,11 @@ describe('SAMLAuthenticationProvider', () => { ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -498,14 +530,20 @@ describe('SAMLAuthenticationProvider', () => { [ 'current session is valid', Promise.resolve( - mockAuthenticatedUser({ authentication_provider: { type: 'saml', name: 'saml' } }) + securityMock.createApiResponse({ + body: mockAuthenticatedUser({ + authentication_provider: { type: 'saml', name: 'saml' }, + }), + }) ), ], [ 'current session is is expired', - Promise.reject(LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())), + Promise.reject( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) + ), ], - ] as Array<[string, Promise]>) { + ] as Array<[string, any]>) { it(`redirects to the home page if ${description}.`, async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { @@ -516,19 +554,19 @@ describe('SAMLAuthenticationProvider', () => { const authorization = `Bearer ${state.accessToken}`; // The first call is made using tokens from existing session. - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(() => response); - // The second call is made using new tokens. - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(mockUser) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockImplementationOnce( + () => response + ); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + authentication: mockUser, + }, + }) ); - - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - authentication: mockUser, - }); - mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect( @@ -549,12 +587,11 @@ describe('SAMLAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -573,13 +610,19 @@ describe('SAMLAuthenticationProvider', () => { const authorization = `Bearer ${state.accessToken}`; // The first call is made using tokens from existing session. - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(() => response); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - authentication: mockUser, - }); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockImplementationOnce( + () => response + ); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + authentication: mockUser, + }, + }) + ); mockOptions.tokens.invalidate.mockResolvedValue(undefined); @@ -610,12 +653,11 @@ describe('SAMLAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -641,16 +683,20 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects requests to the IdP remembering redirect URL with existing state.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - id: 'some-request-id', - redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + id: 'some-request-id', + redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + }, + }) + ); await expect( provider.login( @@ -674,7 +720,9 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/prepare', body: { realm: 'test-realm' }, }); @@ -684,10 +732,14 @@ describe('SAMLAuthenticationProvider', () => { it('redirects requests to the IdP remembering redirect URL without state.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - id: 'some-request-id', - redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + id: 'some-request-id', + redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + }, + }) + ); await expect( provider.login( @@ -711,7 +763,9 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/prepare', body: { realm: 'test-realm' }, }); @@ -721,8 +775,10 @@ describe('SAMLAuthenticationProvider', () => { it('fails if SAML request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest(); - const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login( @@ -735,7 +791,9 @@ describe('SAMLAuthenticationProvider', () => { ) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/prepare', body: { realm: 'test-realm' }, }); }); @@ -791,11 +849,6 @@ describe('SAMLAuthenticationProvider', () => { it('redirects non-AJAX request that can not be authenticated to the "capture URL" page.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - id: 'some-request-id', - redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', - }); - await expect(provider.authenticate(request)).resolves.toEqual( AuthenticationResult.redirectTo( '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=saml&providerName=saml', @@ -803,7 +856,7 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('succeeds if state contains a valid token.', async () => { @@ -833,11 +886,13 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const failureReason = { statusCode: 500, message: 'Token is not valid!' }; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(failureReason); await expect(provider.authenticate(request, state)).resolves.toEqual( - AuthenticationResult.failed(failureReason as any) + AuthenticationResult.failed(failureReason) ); expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); @@ -853,8 +908,8 @@ describe('SAMLAuthenticationProvider', () => { realm: 'test-realm', }; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue({ @@ -889,8 +944,8 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); const refreshFailureReason = { @@ -920,8 +975,8 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -949,8 +1004,8 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -976,8 +1031,8 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -994,7 +1049,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if realm from state is different from the realm provider is configured with.', async () => { @@ -1015,7 +1070,7 @@ describe('SAMLAuthenticationProvider', () => { await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects to logged out view if state is `null` or does not include access token.', async () => { @@ -1028,7 +1083,7 @@ describe('SAMLAuthenticationProvider', () => { DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('fails if SAML logout call fails.', async () => { @@ -1036,8 +1091,10 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.logout(request, { @@ -1047,8 +1104,10 @@ describe('SAMLAuthenticationProvider', () => { }) ).resolves.toEqual(DeauthenticationResult.failed(failureReason)); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -1056,15 +1115,19 @@ describe('SAMLAuthenticationProvider', () => { it('fails if SAML invalidate call fails.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect(provider.logout(request)).resolves.toEqual( DeauthenticationResult.failed(failureReason) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/invalidate', body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1074,7 +1137,9 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: null } }) + ); await expect( provider.logout(request, { @@ -1084,8 +1149,10 @@ describe('SAMLAuthenticationProvider', () => { }) ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -1095,7 +1162,9 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: undefined } }) + ); await expect( provider.logout(request, { @@ -1105,8 +1174,10 @@ describe('SAMLAuthenticationProvider', () => { }) ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -1118,7 +1189,9 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: null } }) + ); await expect( provider.logout(request, { @@ -1128,8 +1201,10 @@ describe('SAMLAuthenticationProvider', () => { }) ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/logout', body: { token: accessToken, refresh_token: refreshToken }, }); }); @@ -1137,7 +1212,9 @@ describe('SAMLAuthenticationProvider', () => { it('relies on SAML invalidate call even if access token is presented.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: null } }) + ); await expect( provider.logout(request, { @@ -1147,8 +1224,10 @@ describe('SAMLAuthenticationProvider', () => { }) ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/invalidate', body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1156,14 +1235,18 @@ describe('SAMLAuthenticationProvider', () => { it('redirects to `loggedOut` URL if `redirect` field in SAML invalidate response is null.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: null } }) + ); await expect(provider.logout(request)).resolves.toEqual( DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/invalidate', body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1171,14 +1254,18 @@ describe('SAMLAuthenticationProvider', () => { it('redirects to `loggedOut` URL if `redirect` field in SAML invalidate response is not defined.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ body: { redirect: undefined } }) + ); await expect(provider.logout(request)).resolves.toEqual( DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/invalidate', body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1190,7 +1277,7 @@ describe('SAMLAuthenticationProvider', () => { DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => { @@ -1198,9 +1285,11 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser.mockResolvedValue({ - redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }, + }) + ); await expect( provider.logout(request, { @@ -1212,15 +1301,17 @@ describe('SAMLAuthenticationProvider', () => { DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); }); it('redirects user to the IdP if SLO is supported by IdP in case of IdP initiated logout.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', - }); + mockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }, + }) + ); await expect( provider.logout(request, { @@ -1232,7 +1323,7 @@ describe('SAMLAuthenticationProvider', () => { DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') ); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index 34639a849d354..58792727de733 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -343,13 +343,19 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/authenticate`. - result = await this.options.client.callAsInternalUser('shield.samlAuthenticate', { - body: { - ids: !isIdPInitiatedLogin ? [stateRequestId] : [], - content: samlResponse, - realm: this.realm, - }, - }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + result = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { + ids: !isIdPInitiatedLogin ? [stateRequestId] : [], + content: samlResponse, + realm: this.realm, + }, + }) + ).body as any; } catch (err) { this.logger.debug(`Failed to log in with SAML response: ${err.message}`); @@ -541,12 +547,15 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/prepare`. - const { id: requestId, redirect } = await this.options.client.callAsInternalUser( - 'shield.samlPrepare', - { + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + const { id: requestId, redirect } = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/saml/prepare', body: { realm: this.realm }, - } - ); + }) + ).body as any; this.logger.debug('Redirecting to Identity Provider with SAML request.'); @@ -570,9 +579,15 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/logout`. - const { redirect } = await this.options.client.callAsInternalUser('shield.samlLogout', { - body: { token: accessToken, refresh_token: refreshToken }, - }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + const { redirect } = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/saml/logout', + body: { token: accessToken, refresh_token: refreshToken }, + }) + ).body as any; this.logger.debug('User session has been successfully invalidated.'); @@ -589,13 +604,19 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`. - const { redirect } = await this.options.client.callAsInternalUser('shield.samlInvalidate', { - // Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`. - body: { - queryString: request.url.search ? request.url.search.slice(1) : '', - realm: this.realm, - }, - }); + // We can replace generic `transport.request` with a dedicated API method call once + // https://github.com/elastic/elasticsearch/issues/67189 is resolved. + const { redirect } = ( + await this.options.client.asInternalUser.transport.request({ + method: 'POST', + path: '/_security/saml/invalidate', + // Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`. + body: { + queryString: request.url.search ? request.url.search.slice(1) : '', + realm: this.realm, + }, + }) + ).body as any; this.logger.debug('User session has been successfully invalidated.'); diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 5a600461ef467..ad100ac5be893 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -5,31 +5,27 @@ */ import Boom from '@hapi/boom'; -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../mocks'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { - LegacyElasticsearchErrorHelpers, - ILegacyClusterClient, - ScopeableRequest, -} from '../../../../../../src/core/server'; +import { ScopeableRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { TokenAuthenticationProvider } from './token'; function expectAuthenticateCall( - mockClusterClient: jest.Mocked, + mockClusterClient: ReturnType, scopeableRequest: ScopeableRequest ) { expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes(1); } describe('TokenAuthenticationProvider', () => { @@ -51,11 +47,15 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser.mockResolvedValue({ - access_token: tokenPair.accessToken, - refresh_token: tokenPair.refreshToken, - authentication: user, - }); + mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + authentication: user, + }, + }) + ); await expect(provider.login(request, credentials)).resolves.toEqual( AuthenticationResult.succeeded( @@ -65,8 +65,8 @@ describe('TokenAuthenticationProvider', () => { ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: 'password', ...credentials }, }); }); @@ -75,17 +75,18 @@ describe('TokenAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const credentials = { username: 'user', password: 'password' }; - const authenticationError = new Error('Invalid credentials'); - mockOptions.client.callAsInternalUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + mockOptions.client.asInternalUser.security.getToken.mockRejectedValue(authenticationError); await expect(provider.login(request, credentials)).resolves.toEqual( AuthenticationResult.failed(authenticationError) ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asInternalUser.security.getToken).toHaveBeenCalledWith({ body: { grant_type: 'password', ...credentials }, }); @@ -158,8 +159,10 @@ describe('TokenAuthenticationProvider', () => { const user = mockAuthenticatedUser(); const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( + securityMock.createApiResponse({ body: user }) + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( @@ -179,9 +182,9 @@ describe('TokenAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -212,9 +215,13 @@ describe('TokenAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = `Bearer ${tokenPair.accessToken}`; - const authenticationError = new errors.InternalServerError('something went wrong'); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + const authenticationError = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + authenticationError + ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( @@ -231,13 +238,15 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const refreshError = new errors.InternalServerError('failed to refresh token'); + const refreshError = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); mockOptions.tokens.refresh.mockRejectedValue(refreshError); await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( @@ -257,9 +266,9 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -288,9 +297,9 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -319,9 +328,9 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 67c2d244e75a2..3afbc25122a26 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -7,6 +7,8 @@ import Boom from '@hapi/boom'; import { KibanaRequest } from '../../../../../../src/core/server'; import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; +import { AuthenticationInfo } from '../../elasticsearch'; +import { getDetailedErrorMessage } from '../../errors'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { canRedirectRequest } from '../can_redirect_request'; @@ -65,9 +67,13 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { access_token: accessToken, refresh_token: refreshToken, authentication: authenticationInfo, - } = await this.options.client.callAsInternalUser('shield.getAccessToken', { - body: { grant_type: 'password', username, password }, - }); + } = ( + await this.options.client.asInternalUser.security.getToken<{ + access_token: string; + refresh_token: string; + authentication: AuthenticationInfo; + }>({ body: { grant_type: 'password', username, password } }) + ).body; this.logger.debug('Get token API request to Elasticsearch successful'); return AuthenticationResult.succeeded( @@ -80,7 +86,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { } ); } catch (err) { - this.logger.debug(`Failed to perform a login: ${err.message}`); + this.logger.debug(`Failed to perform a login: ${getDetailedErrorMessage(err)}`); return AuthenticationResult.failed(err); } } diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 18fdcf8608d29..eb439d74a46d0 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -4,25 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { securityMock } from '../mocks'; -import { - ILegacyClusterClient, - LegacyElasticsearchErrorHelpers, -} from '../../../../../src/core/server'; +import { ElasticsearchClient } from '../../../../../src/core/server'; import { Tokens } from './tokens'; describe('Tokens', () => { let tokens: Tokens; - let mockClusterClient: jest.Mocked; + let mockElasticsearchClient: DeeplyMockedKeys; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + mockElasticsearchClient = elasticsearchServiceMock.createElasticsearchClient(); const tokensOptions = { - client: mockClusterClient, + client: mockElasticsearchClient, logger: loggingSystemMock.create().get(), }; @@ -33,9 +32,24 @@ describe('Tokens', () => { const nonExpirationErrors = [ {}, new Error(), - new errors.InternalServerError(), - new errors.Forbidden(), + new errors.NoLivingConnectionsError( + 'Server is not available', + securityMock.createApiResponse({ body: {} }) + ), + new errors.ResponseError( + securityMock.createApiResponse({ + statusCode: 403, + body: { error: { reason: 'forbidden' } }, + }) + ), { statusCode: 500, body: { error: { reason: 'some unknown reason' } } }, + new errors.NoLivingConnectionsError( + 'Server is not available', + securityMock.createApiResponse({ + statusCode: 500, + body: { error: { reason: 'some unknown reason' } }, + }) + ), ]; for (const error of nonExpirationErrors) { expect(Tokens.isAccessTokenExpiredError(error)).toBe(false); @@ -43,8 +57,10 @@ describe('Tokens', () => { const expirationErrors = [ { statusCode: 401 }, - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()), - new errors.AuthenticationException(), + securityMock.createApiResponse({ + statusCode: 401, + body: { error: { reason: 'unauthenticated' } }, + }), ]; for (const error of expirationErrors) { expect(Tokens.isAccessTokenExpiredError(error)).toBe(true); @@ -55,25 +71,30 @@ describe('Tokens', () => { const refreshToken = 'some-refresh-token'; it('throws if API call fails with unknown reason', async () => { - const refreshFailureReason = new errors.ServiceUnavailable('Server is not available'); - mockClusterClient.callAsInternalUser.mockRejectedValue(refreshFailureReason); + const refreshFailureReason = new errors.NoLivingConnectionsError( + 'Server is not available', + securityMock.createApiResponse({ body: {} }) + ); + mockElasticsearchClient.security.getToken.mockRejectedValue(refreshFailureReason); await expect(tokens.refresh(refreshToken)).rejects.toBe(refreshFailureReason); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledWith({ body: { grant_type: 'refresh_token', refresh_token: refreshToken }, }); }); it('returns `null` if refresh token is not valid', async () => { - const refreshFailureReason = new errors.BadRequest(); - mockClusterClient.callAsInternalUser.mockRejectedValue(refreshFailureReason); + const refreshFailureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 400, body: {} }) + ); + mockElasticsearchClient.security.getToken.mockRejectedValue(refreshFailureReason); await expect(tokens.refresh(refreshToken)).resolves.toBe(null); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledWith({ body: { grant_type: 'refresh_token', refresh_token: refreshToken }, }); }); @@ -81,19 +102,23 @@ describe('Tokens', () => { it('returns token pair if refresh API call succeeds', async () => { const authenticationInfo = mockAuthenticatedUser(); const tokenPair = { accessToken: 'access-token', refreshToken: 'refresh-token' }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ - access_token: tokenPair.accessToken, - refresh_token: tokenPair.refreshToken, - authentication: authenticationInfo, - }); + mockElasticsearchClient.security.getToken.mockResolvedValue( + securityMock.createApiResponse({ + body: { + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + authentication: authenticationInfo, + }, + }) + ); await expect(tokens.refresh(refreshToken)).resolves.toEqual({ authenticationInfo, ...tokenPair, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.security.getToken).toHaveBeenCalledWith({ body: { grant_type: 'refresh_token', refresh_token: refreshToken }, }); }); @@ -101,158 +126,165 @@ describe('Tokens', () => { describe('invalidate()', () => { for (const [description, failureReason] of [ - ['an unknown error', new Error('failed to delete token')], - ['a 404 error without body', { statusCode: 404 }], + [ + 'an unknown error', + new errors.ResponseError( + securityMock.createApiResponse( + securityMock.createApiResponse({ body: { message: 'failed to delete token' } }) + ) + ), + ], + [ + 'a 404 error without body', + new errors.ResponseError( + securityMock.createApiResponse( + securityMock.createApiResponse({ statusCode: 404, body: {} }) + ) + ), + ], ] as Array<[string, object]>) { it(`throws if call to delete access token responds with ${description}`, async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + mockElasticsearchClient.security.invalidateToken.mockImplementation((args: any) => { if (args && args.body && args.body.token) { - return Promise.reject(failureReason); + return Promise.reject(failureReason) as any; } - return Promise.resolve({ invalidated_tokens: 1 }); + return Promise.resolve( + securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + ) as any; }); await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { token: tokenPair.accessToken }, - } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { refresh_token: tokenPair.refreshToken }, - } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(2); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); it(`throws if call to delete refresh token responds with ${description}`, async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + mockElasticsearchClient.security.invalidateToken.mockImplementation((args: any) => { if (args && args.body && args.body.refresh_token) { - return Promise.reject(failureReason); + return Promise.reject(failureReason) as any; } - return Promise.resolve({ invalidated_tokens: 1 }); + return Promise.resolve( + securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + ) as any; }); await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { token: tokenPair.accessToken }, - } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { refresh_token: tokenPair.refreshToken }, - } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(2); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); } it('invalidates all provided tokens', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 1 }); + mockElasticsearchClient.security.invalidateToken.mockResolvedValue( + securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(2); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); it('invalidates only access token if only access token is provided', async () => { const tokenPair = { accessToken: 'foo' }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 1 }); + mockElasticsearchClient.security.invalidateToken.mockResolvedValue( + securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); }); it('invalidates only refresh token if only refresh token is provided', async () => { const tokenPair = { refreshToken: 'foo' }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 1 }); + mockElasticsearchClient.security.invalidateToken.mockResolvedValue( + securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); for (const [description, response] of [ - ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })], + [ + 'none of the tokens were invalidated', + Promise.resolve(securityMock.createApiResponse({ body: { invalidated_tokens: 0 } })), + ], [ '404 error is returned', - Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }), + Promise.resolve( + securityMock.createApiResponse({ statusCode: 404, body: { invalidated_tokens: 0 } }) + ), ], - ] as Array<[string, Promise]>) { + ] as Array<[string, any]>) { it(`does not fail if ${description}`, async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockClusterClient.callAsInternalUser.mockImplementation(() => response); + mockElasticsearchClient.security.invalidateToken.mockImplementation(() => response); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { token: tokenPair.accessToken }, - } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { - body: { refresh_token: tokenPair.refreshToken }, - } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(2); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); } it('does not fail if more than one token per access or refresh token were invalidated', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 5 }); + mockElasticsearchClient.security.invalidateToken.mockResolvedValue( + securityMock.createApiResponse({ body: { invalidated_tokens: 5 } }) + ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledTimes(2); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { token: tokenPair.accessToken }, + }); + expect(mockElasticsearchClient.security.invalidateToken).toHaveBeenCalledWith({ + body: { refresh_token: tokenPair.refreshToken }, + }); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index a435452ae112f..7bee3dfe1c5a0 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient, Logger } from '../../../../../src/core/server'; +import type { ElasticsearchClient, Logger } from '../../../../../src/core/server'; import type { AuthenticationInfo } from '../elasticsearch'; import { getErrorStatusCode } from '../errors'; @@ -42,9 +42,7 @@ export class Tokens { */ private readonly logger: Logger; - constructor( - private readonly options: Readonly<{ client: ILegacyClusterClient; logger: Logger }> - ) { + constructor(private readonly options: Readonly<{ client: ElasticsearchClient; logger: Logger }>) { this.logger = options.logger; } @@ -59,9 +57,13 @@ export class Tokens { access_token: accessToken, refresh_token: refreshToken, authentication: authenticationInfo, - } = await this.options.client.callAsInternalUser('shield.getAccessToken', { - body: { grant_type: 'refresh_token', refresh_token: existingRefreshToken }, - }); + } = ( + await this.options.client.security.getToken<{ + access_token: string; + refresh_token: string; + authentication: AuthenticationInfo; + }>({ body: { grant_type: 'refresh_token', refresh_token: existingRefreshToken } }) + ).body; this.logger.debug('Access token has been successfully refreshed.'); @@ -108,10 +110,10 @@ export class Tokens { let invalidatedTokensCount; try { invalidatedTokensCount = ( - await this.options.client.callAsInternalUser('shield.deleteAccessToken', { + await this.options.client.security.invalidateToken<{ invalidated_tokens: number }>({ body: { refresh_token: refreshToken }, }) - ).invalidated_tokens; + ).body.invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate refresh token: ${err.message}`); @@ -140,10 +142,10 @@ export class Tokens { let invalidatedTokensCount; try { invalidatedTokensCount = ( - await this.options.client.callAsInternalUser('shield.deleteAccessToken', { + await this.options.client.security.invalidateToken<{ invalidated_tokens: number }>({ body: { token: accessToken }, }) - ).invalidated_tokens; + ).body.invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate access token: ${err.message}`); diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts deleted file mode 100644 index 0aaad251ae642..0000000000000 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function elasticsearchClientPlugin(Client: any, config: unknown, components: any) { - const ca = components.clientAction.factory; - - Client.prototype.shield = components.clientAction.namespaceFactory(); - const shield = Client.prototype.shield.prototype; - - /** - * Perform a [shield.authenticate](Retrieve details about the currently authenticated user) request - * - * @param {Object} params - An object with parameters used to carry out this action - */ - shield.authenticate = ca({ - params: {}, - url: { - fmt: '/_security/_authenticate', - }, - }); - - /** - * Asks Elasticsearch to prepare SAML authentication request to be sent to - * the 3rd-party SAML identity provider. - * - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL - * in the Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm. - * - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{realm: string, id: string, redirect: string}} Object that includes identifier - * of the SAML realm used to prepare authentication request, encrypted request token to be - * sent to Elasticsearch with SAML response and redirect URL to the identity provider that - * will be used to authenticate user. - */ - shield.samlPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/prepare', - }, - }); - - /** - * Sends SAML response returned by identity provider to Elasticsearch for validation. - * - * @param {Array.} ids A list of encrypted request tokens returned within SAML - * preparation response. - * @param {string} content SAML response returned by identity provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.samlAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/authenticate', - }, - }); - - /** - * Invalidates SAML access token. - * - * @param {string} token SAML access token that needs to be invalidated. - * - * @returns {{redirect?: string}} - */ - shield.samlLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/logout', - }, - }); - - /** - * Invalidates SAML session based on Logout Request received from the Identity Provider. - * - * @param {string} queryString URL encoded query string provided by Identity Provider. - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL in the - * Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm to invalidate session. - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{redirect?: string}} - */ - shield.samlInvalidate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/invalidate', - }, - }); - - /** - * Asks Elasticsearch to prepare an OpenID Connect authentication request to be sent to - * the 3rd-party OpenID Connect provider. - * - * @param {string} realm The OpenID Connect realm name in Elasticsearch - * - * @returns {{state: string, nonce: string, redirect: string}} Object that includes two opaque parameters that need - * to be sent to Elasticsearch with the OpenID Connect response and redirect URL to the OpenID Connect provider that - * will be used to authenticate user. - */ - shield.oidcPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/prepare', - }, - }); - - /** - * Sends the URL to which the OpenID Connect Provider redirected the UA to Elasticsearch for validation. - * - * @param {string} state The state parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} nonce The nonce parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} redirect_uri The URL to where the UA was redirected by the OpenID Connect provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, refresh_token; string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.oidcAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/authenticate', - }, - }); - - /** - * Invalidates an access token and refresh token pair that was generated after an OpenID Connect authentication. - * - * @param {string} token An access token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * @param {string} refresh_token A refresh token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * - * @returns {{redirect?: string}} If the Elasticsearch OpenID Connect realm configuration and the - * OpenID Connect provider supports RP-initiated SLO, a URL to redirect the UA - */ - shield.oidcLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/logout', - }, - }); - - /** - * Refreshes an access token. - * - * @param {string} grant_type Currently only "refresh_token" grant type is supported. - * @param {string} refresh_token One-time refresh token that will be exchanged to the new access/refresh token pair. - * - * @returns {{access_token: string, type: string, expires_in: number, refresh_token: string}} - */ - shield.getAccessToken = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oauth2/token', - }, - }); - - /** - * Invalidates an access token. - * - * @param {string} token The access token to invalidate - * - * @returns {{created: boolean}} - */ - shield.deleteAccessToken = ca({ - method: 'DELETE', - needBody: true, - params: { - token: { - type: 'string', - }, - }, - url: { - fmt: '/_security/oauth2/token', - }, - }); - - /** - * Gets an access token in exchange to the certificate chain for the target subject distinguished name. - * - * @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not - * base64url-encoded) DER PKIX certificate values. - * - * @returns {{access_token: string, type: string, expires_in: number}} - */ - shield.delegatePKI = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/delegate_pki', - }, - }); -} diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.test.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.test.ts index 812e3e3d17f99..e58cc4b2caa52 100644 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.test.ts +++ b/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.test.ts @@ -5,20 +5,11 @@ */ import { BehaviorSubject } from 'rxjs'; -import { - ILegacyCustomClusterClient, - ServiceStatusLevels, - CoreStatus, -} from '../../../../../src/core/server'; +import { ServiceStatusLevels, CoreStatus } from '../../../../../src/core/server'; import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing'; -import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { ElasticsearchService } from './elasticsearch_service'; -import { - coreMock, - elasticsearchServiceMock, - loggingSystemMock, -} from '../../../../../src/core/server/mocks'; +import { coreMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { nextTick } from '@kbn/test/jest'; @@ -30,35 +21,20 @@ describe('ElasticsearchService', () => { describe('setup()', () => { it('exposes proper contract', () => { - const mockCoreSetup = coreMock.createSetup(); - const mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient(); - mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - expect( service.setup({ - elasticsearch: mockCoreSetup.elasticsearch, - status: mockCoreSetup.status, + status: coreMock.createSetup().status, license: licenseMock.create(), }) - ).toEqual({ clusterClient: mockClusterClient }); - - expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledTimes(1); - expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledWith('security', { - plugins: [elasticsearchClientPlugin], - }); + ).toBeUndefined(); }); }); describe('start()', () => { - let mockClusterClient: ILegacyCustomClusterClient; let mockLicense: jest.Mocked; let mockStatusSubject: BehaviorSubject; let mockLicenseSubject: BehaviorSubject; beforeEach(() => { - const mockCoreSetup = coreMock.createSetup(); - mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient(); - mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - mockLicenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); mockLicense = licenseMock.create(); mockLicense.isEnabled.mockReturnValue(false); @@ -71,20 +47,18 @@ describe('ElasticsearchService', () => { }, savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, }); - mockCoreSetup.status.core$ = mockStatusSubject; + + const mockStatus = coreMock.createSetup().status; + mockStatus.core$ = mockStatusSubject; service.setup({ - elasticsearch: mockCoreSetup.elasticsearch, - status: mockCoreSetup.status, + status: mockStatus, license: mockLicense, }); }); it('exposes proper contract', () => { - expect(service.start()).toEqual({ - clusterClient: mockClusterClient, - watchOnlineStatus$: expect.any(Function), - }); + expect(service.start()).toEqual({ watchOnlineStatus$: expect.any(Function) }); }); it('`watchOnlineStatus$` allows tracking of Elasticsearch status', () => { @@ -199,24 +173,4 @@ describe('ElasticsearchService', () => { expect(mockHandler).toHaveBeenCalledTimes(2); }); }); - - describe('stop()', () => { - it('properly closes cluster client instance', () => { - const mockCoreSetup = coreMock.createSetup(); - const mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient(); - mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - - service.setup({ - elasticsearch: mockCoreSetup.elasticsearch, - status: mockCoreSetup.status, - license: licenseMock.create(), - }); - - expect(mockClusterClient.close).not.toHaveBeenCalled(); - - service.stop(); - - expect(mockClusterClient.close).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.ts index 42a83b2e5b527..ace1dc553890d 100644 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.ts +++ b/x-pack/plugins/security/server/elasticsearch/elasticsearch_service.ts @@ -6,29 +6,15 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, shareReplay, tap } from 'rxjs/operators'; -import { - ILegacyClusterClient, - ILegacyCustomClusterClient, - Logger, - ServiceStatusLevels, - StatusServiceSetup, - ElasticsearchServiceSetup as CoreElasticsearchServiceSetup, -} from '../../../../../src/core/server'; +import { Logger, ServiceStatusLevels, StatusServiceSetup } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; -import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; export interface ElasticsearchServiceSetupParams { - readonly elasticsearch: CoreElasticsearchServiceSetup; readonly status: StatusServiceSetup; readonly license: SecurityLicense; } -export interface ElasticsearchServiceSetup { - readonly clusterClient: ILegacyClusterClient; -} - export interface ElasticsearchServiceStart { - readonly clusterClient: ILegacyClusterClient; readonly watchOnlineStatus$: () => Observable; } @@ -41,22 +27,13 @@ export interface OnlineStatusRetryScheduler { */ export class ElasticsearchService { readonly #logger: Logger; - #clusterClient?: ILegacyCustomClusterClient; #coreStatus$!: Observable; constructor(logger: Logger) { this.#logger = logger; } - setup({ - elasticsearch, - status, - license, - }: ElasticsearchServiceSetupParams): ElasticsearchServiceSetup { - this.#clusterClient = elasticsearch.legacy.createClient('security', { - plugins: [elasticsearchClientPlugin], - }); - + setup({ status, license }: ElasticsearchServiceSetupParams) { this.#coreStatus$ = combineLatest([status.core$, license.features$]).pipe( map( ([coreStatus]) => @@ -64,14 +41,10 @@ export class ElasticsearchService { ), shareReplay(1) ); - - return { clusterClient: this.#clusterClient }; } start(): ElasticsearchServiceStart { return { - clusterClient: this.#clusterClient!, - // We'll need to get rid of this as soon as Core's Elasticsearch service exposes this // functionality in the scope of https://github.com/elastic/kibana/issues/41983. watchOnlineStatus$: () => { @@ -120,11 +93,4 @@ export class ElasticsearchService { }, }; } - - stop() { - if (this.#clusterClient) { - this.#clusterClient.close(); - this.#clusterClient = undefined; - } - } } diff --git a/x-pack/plugins/security/server/elasticsearch/index.ts b/x-pack/plugins/security/server/elasticsearch/index.ts index 23e4876904c31..c770600db44cd 100644 --- a/x-pack/plugins/security/server/elasticsearch/index.ts +++ b/x-pack/plugins/security/server/elasticsearch/index.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuthenticatedUser } from '../../common/model'; +import type { AuthenticatedUser } from '../../common/model'; export type AuthenticationInfo = Omit; export { ElasticsearchService, - ElasticsearchServiceSetup, ElasticsearchServiceStart, OnlineStatusRetryScheduler, } from './elasticsearch_service'; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index df30d1bf9d6f6..7d8f3cf36a4ad 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -14,7 +14,7 @@ function createSetupMock() { const mockAuthz = authorizationMock.create(); return { audit: auditServiceMock.create(), - authc: authenticationServiceMock.createSetup(), + authc: { getCurrentUser: jest.fn() }, authz: { actions: mockAuthz.actions, checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 54efdbdccbb77..256eca376fa02 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -6,11 +6,10 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { ILegacyCustomClusterClient } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; -import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; +import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; @@ -19,7 +18,6 @@ describe('Security Plugin', () => { let plugin: Plugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; - let mockClusterClient: jest.Mocked; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { @@ -43,9 +41,6 @@ describe('Security Plugin', () => { protocol: 'https', }); - mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient(); - mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - mockSetupDependencies = ({ licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, features: featuresPluginMock.createSetup(), diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 1016221cb719d..8d8e4c096f37e 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -12,6 +12,7 @@ import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { CoreSetup, CoreStart, + KibanaRequest, Logger, PluginInitializerContext, } from '../../../../src/core/server'; @@ -24,22 +25,19 @@ import { import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { - AuthenticationService, - AuthenticationServiceSetup, - AuthenticationServiceStart, -} from './authentication'; +import { AuthenticationService, AuthenticationServiceStart } from './authentication'; import { AuthorizationService, AuthorizationServiceSetup } from './authorization'; import { AnonymousAccessService, AnonymousAccessServiceStart } from './anonymous_access'; import { ConfigSchema, ConfigType, createConfig } from './config'; import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; +import { AuthenticatedUser } from '../common/model'; import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; import { securityFeatures } from './features'; import { ElasticsearchService } from './elasticsearch'; -import { SessionManagementService } from './session_management'; +import { Session, SessionManagementService } from './session_management'; import { registerSecurityUsageCollector } from './usage_collector'; import { setupSpacesClient } from './spaces'; @@ -60,7 +58,7 @@ export interface SecurityPluginSetup { /** * @deprecated Use `authc` methods from the `SecurityServiceStart` contract instead. */ - authc: Pick; + authc: { getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null }; /** * @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead. */ @@ -104,8 +102,8 @@ export interface PluginStartDependencies { */ export class Plugin { private readonly logger: Logger; - private authenticationStart?: AuthenticationServiceStart; private authorizationSetup?: AuthorizationServiceSetup; + private auditSetup?: AuditServiceSetup; private anonymousAccessStart?: AnonymousAccessServiceStart; private configSubscription?: Subscription; @@ -117,6 +115,14 @@ export class Plugin { return this.config; }; + private session?: Session; + private readonly getSession = () => { + if (!this.session) { + throw new Error('Session is not available.'); + } + return this.session; + }; + private kibanaIndexName?: string; private readonly getKibanaIndexName = () => { if (!this.kibanaIndexName) { @@ -125,6 +131,17 @@ export class Plugin { return this.kibanaIndexName; }; + private readonly authenticationService = new AuthenticationService( + this.initializerContext.logger.get('authentication') + ); + private authenticationStart?: AuthenticationServiceStart; + private readonly getAuthentication = () => { + if (!this.authenticationStart) { + throw new Error(`authenticationStart is not registered!`); + } + return this.authenticationStart; + }; + private readonly featureUsageService = new SecurityFeatureUsageService(); private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; private readonly getFeatureUsageService = () => { @@ -143,9 +160,6 @@ export class Plugin { private readonly sessionManagementService = new SessionManagementService( this.initializerContext.logger.get('session') ); - private readonly authenticationService = new AuthenticationService( - this.initializerContext.logger.get('authentication') - ); private readonly anonymousAccessService = new AnonymousAccessService( this.initializerContext.logger.get('anonymous-access'), this.getConfig @@ -211,46 +225,22 @@ export class Plugin { features.registerElasticsearchFeature(securityFeature) ); - const { clusterClient } = this.elasticsearchService.setup({ - elasticsearch: core.elasticsearch, - license, - status: core.status, - }); - + this.elasticsearchService.setup({ license, status: core.status }); this.featureUsageService.setup({ featureUsage: licensing.featureUsage }); + this.sessionManagementService.setup({ config, http: core.http, taskManager }); + this.authenticationService.setup({ http: core.http, license }); registerSecurityUsageCollector({ usageCollection, config, license }); - const { session } = this.sessionManagementService.setup({ - config, - clusterClient, - http: core.http, - kibanaIndexName, - taskManager, - }); - - const audit = this.auditService.setup({ + this.auditSetup = this.auditService.setup({ license, config: config.audit, logging: core.logging, http: core.http, getSpaceId: (request) => spaces?.spacesService.getSpaceId(request), - getSID: (request) => session.getSID(request), - getCurrentUser: (request) => authenticationSetup.getCurrentUser(request), - recordAuditLoggingUsage: () => this.featureUsageServiceStart?.recordAuditLoggingUsage(), - }); - const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); - - const authenticationSetup = this.authenticationService.setup({ - legacyAuditLogger, - audit, - getFeatureUsageService: this.getFeatureUsageService, - http: core.http, - clusterClient, - config, - license, - loggers: this.initializerContext.logger, - session, + getSID: (request) => this.getSession().getSID(request), + getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request), + recordAuditLoggingUsage: () => this.getFeatureUsageService().recordAuditLoggingUsage(), }); this.anonymousAccessService.setup(); @@ -267,18 +257,18 @@ export class Plugin { buildNumber: this.initializerContext.env.packageInfo.buildNum, getSpacesService: () => spaces?.spacesService, features, - getCurrentUser: authenticationSetup.getCurrentUser, + getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request), }); setupSpacesClient({ spaces, - audit, + audit: this.auditSetup, authz: this.authorizationSetup, }); setupSavedObjects({ - legacyAuditLogger, - audit, + legacyAuditLogger: new SecurityAuditLogger(this.auditSetup.getLogger()), + audit: this.auditSetup, authz: this.authorizationSetup, savedObjects: core.savedObjects, getSpacesService: () => spaces?.spacesService, @@ -292,26 +282,20 @@ export class Plugin { config, authz: this.authorizationSetup, license, - session, + getSession: this.getSession, getFeatures: () => startServicesPromise.then((services) => services.features.getKibanaFeatures()), getFeatureUsageService: this.getFeatureUsageService, - getAuthenticationService: () => { - if (!this.authenticationStart) { - throw new Error('Authentication service is not started!'); - } - - return this.authenticationStart; - }, + getAuthenticationService: this.getAuthentication, }); return Object.freeze({ audit: { - asScoped: audit.asScoped, - getLogger: audit.getLogger, + asScoped: this.auditSetup.asScoped, + getLogger: this.auditSetup.getLogger, }, - authc: { getCurrentUser: authenticationSetup.getCurrentUser }, + authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) }, authz: { actions: this.authorizationSetup.actions, @@ -337,11 +321,24 @@ export class Plugin { const clusterClient = core.elasticsearch.client; const { watchOnlineStatus$ } = this.elasticsearchService.start(); + const { session } = this.sessionManagementService.start({ + elasticsearchClient: clusterClient.asInternalUser, + kibanaIndexName: this.getKibanaIndexName(), + online$: watchOnlineStatus$(), + taskManager, + }); + this.session = session; - this.sessionManagementService.start({ online$: watchOnlineStatus$(), taskManager }); + const config = this.getConfig(); this.authenticationStart = this.authenticationService.start({ - http: core.http, + audit: this.auditSetup!, clusterClient, + config, + featureUsageService: this.featureUsageServiceStart, + http: core.http, + legacyAuditLogger: new SecurityAuditLogger(this.auditSetup!.getLogger()), + loggers: this.initializerContext.logger, + session, }); this.authorizationService.start({ features, clusterClient, online$: watchOnlineStatus$() }); @@ -391,7 +388,6 @@ export class Plugin { this.securityLicenseService.stop(); this.auditService.stop(); this.authorizationService.stop(); - this.elasticsearchService.stop(); this.sessionManagementService.stop(); } } diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index 4103594faba15..f7b51eeffe6ed 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -32,7 +32,7 @@ export const routeDefinitionParamsMock = { httpResources: httpResourcesMock.createRegistrar(), getFeatures: jest.fn(), getFeatureUsageService: jest.fn(), - session: sessionMock.create(), + getSession: jest.fn().mockReturnValue(sessionMock.create()), getAuthenticationService: jest.fn().mockReturnValue(authenticationServiceMock.createStart()), } as unknown) as DeeplyMockedKeys), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 2d49329fd63d3..9a8fe2e0d6d15 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -33,7 +33,7 @@ export interface RouteDefinitionParams { logger: Logger; config: ConfigType; authz: AuthorizationServiceSetup; - session: PublicMethodsOf; + getSession: () => PublicMethodsOf; license: SecurityLicense; getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index c51956f3fe530..b068e80cfa859 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -24,7 +24,9 @@ describe('Info session routes', () => { beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; - session = routeParamsMock.session; + + session = sessionMock.create(); + routeParamsMock.getSession.mockReturnValue(session); defineSessionInfoRoutes(routeParamsMock); }); diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index 381127284f780..1f73edf510976 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -10,12 +10,12 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the session info. */ -export function defineSessionInfoRoutes({ router, logger, session }: RouteDefinitionParams) { +export function defineSessionInfoRoutes({ router, logger, getSession }: RouteDefinitionParams) { router.get( { path: '/internal/security/session', validate: false }, async (_context, request, response) => { try { - const sessionValue = await session.get(request); + const sessionValue = await getSession().get(request); if (sessionValue) { return response.ok({ body: { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index 24e73e456619b..2c7c3f6bafc40 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -48,7 +48,10 @@ describe('Change password', () => { beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; - session = routeParamsMock.session; + + session = sessionMock.create(); + routeParamsMock.getSession.mockReturnValue(session); + authc = authenticationServiceMock.createStart(); routeParamsMock.getAuthenticationService.mockReturnValue(authc); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index 7b53afceb48fd..1c9086862c37d 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -16,7 +16,7 @@ import { RouteDefinitionParams } from '..'; export function defineChangeUserPasswordRoutes({ getAuthenticationService, - session, + getSession, router, }: RouteDefinitionParams) { router.post( @@ -37,7 +37,7 @@ export function defineChangeUserPasswordRoutes({ const currentUser = getAuthenticationService().getCurrentUser(request); const isUserChangingOwnPassword = currentUser && currentUser.username === username && canUserChangePassword(currentUser); - const currentSession = isUserChangingOwnPassword ? await session.get(request) : null; + const currentSession = isUserChangingOwnPassword ? await getSession().get(request) : null; // If user is changing their own password they should provide a proof of knowledge their // current password via sending it in `Authorization: Basic base64(username:current password)` diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index 4c4f8a22eee23..a471f5f4e84cb 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -33,10 +33,12 @@ describe('Access agreement view routes', () => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; httpResources = routeParamsMock.httpResources; - session = routeParamsMock.session; config = routeParamsMock.config; license = routeParamsMock.license; + session = sessionMock.create(); + routeParamsMock.getSession.mockReturnValue(session); + license.getFeatures.mockReturnValue({ allowAccessAgreement: true, } as SecurityLicenseFeatures); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index 80a1c2a20cf59..c7f694eca68ce 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -12,7 +12,7 @@ import { RouteDefinitionParams } from '..'; * Defines routes required for the Access Agreement view. */ export function defineAccessAgreementRoutes({ - session, + getSession, httpResources, license, config, @@ -46,7 +46,7 @@ export function defineAccessAgreementRoutes({ // authenticated with the help of HTTP authentication), that means we should safely check if // we have it and can get a corresponding configuration. try { - const sessionValue = await session.get(request); + const sessionValue = await getSession().get(request); const accessAgreement = (sessionValue && config.authc.providers[ diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 31096bc33d686..7cc534663e2f9 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -18,7 +18,8 @@ describe('LoggedOut view routes', () => { let routeConfig: RouteConfig; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); - session = routeParamsMock.session; + session = sessionMock.create(); + routeParamsMock.getSession.mockReturnValue(session); defineLoggedOutRoutes(routeParamsMock); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.ts b/x-pack/plugins/security/server/routes/views/logged_out.ts index b35154e6a0f2a..97357118907d3 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.ts @@ -17,7 +17,7 @@ import { RouteDefinitionParams } from '..'; */ export function defineLoggedOutRoutes({ logger, - session, + getSession, httpResources, basePath, }: RouteDefinitionParams) { @@ -30,7 +30,7 @@ export function defineLoggedOutRoutes({ async (context, request, response) => { // Authentication flow isn't triggered automatically for this route, so we should explicitly // check whether user has an active session already. - const isUserAlreadyLoggedIn = (await session.get(request)) !== null; + const isUserAlreadyLoggedIn = (await getSession().get(request)) !== null; if (isUserAlreadyLoggedIn) { logger.debug('User is already authenticated, redirecting...'); return response.redirected({ diff --git a/x-pack/plugins/security/server/session_management/index.ts b/x-pack/plugins/security/server/session_management/index.ts index ee7ed914947a0..1d256885f49f2 100644 --- a/x-pack/plugins/security/server/session_management/index.ts +++ b/x-pack/plugins/security/server/session_management/index.ts @@ -6,6 +6,6 @@ export { Session, SessionValue } from './session'; export { - SessionManagementServiceSetup, + SessionManagementServiceStart, SessionManagementService, } from './session_management_service'; diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index 1dd47c7ff66e8..51abcfe00253c 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -4,27 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient } from '../../../../../src/core/server'; +import { errors } from '@elastic/elasticsearch'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import { ElasticsearchClient } from '../../../../../src/core/server'; import { ConfigSchema, createConfig } from '../config'; import { getSessionIndexTemplate, SessionIndex } from './session_index'; import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { securityMock } from '../mocks'; import { sessionIndexMock } from './session_index.mock'; describe('Session index', () => { - let mockClusterClient: jest.Mocked; + let mockElasticsearchClient: DeeplyMockedKeys; let sessionIndex: SessionIndex; const indexName = '.kibana_some_tenant_security_session_1'; const indexTemplateName = '.kibana_some_tenant_security_session_index_template_1'; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + mockElasticsearchClient = elasticsearchServiceMock.createElasticsearchClient(); const sessionIndexOptions = { logger: loggingSystemMock.createLogger(), kibanaIndexName: '.kibana_some_tenant', config: createConfig(ConfigSchema.validate({}), loggingSystemMock.createLogger(), { isTLSEnabled: false, }), - clusterClient: mockClusterClient, + elasticsearchClient: mockElasticsearchClient, }; sessionIndex = new SessionIndex(sessionIndexOptions); @@ -32,22 +35,21 @@ describe('Session index', () => { describe('#initialize', () => { function assertExistenceChecksPerformed() { - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsTemplate', { + expect(mockElasticsearchClient.indices.existsTemplate).toHaveBeenCalledWith({ name: indexTemplateName, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.exists', { + expect(mockElasticsearchClient.indices.exists).toHaveBeenCalledWith({ index: getSessionIndexTemplate(indexName).index_patterns, }); } it('debounces initialize calls', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate' || method === 'indices.exists') { - return true; - } - - throw new Error('Unexpected call'); - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); await Promise.all([ sessionIndex.initialize(), @@ -56,112 +58,102 @@ describe('Session index', () => { sessionIndex.initialize(), ]); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); assertExistenceChecksPerformed(); }); it('creates neither index template nor index if they exist', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate' || method === 'indices.exists') { - return true; - } - - throw new Error('Unexpected call'); - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); await sessionIndex.initialize(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); assertExistenceChecksPerformed(); }); it('creates both index template and index if they do not exist', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate' || method === 'indices.exists') { - return false; - } - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: false }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: false }) + ); await sessionIndex.initialize(); const expectedIndexTemplate = getSessionIndexTemplate(indexName); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(4); assertExistenceChecksPerformed(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.putTemplate', { + expect(mockElasticsearchClient.indices.putTemplate).toHaveBeenCalledWith({ name: indexTemplateName, body: expectedIndexTemplate, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { + expect(mockElasticsearchClient.indices.create).toHaveBeenCalledWith({ index: expectedIndexTemplate.index_patterns, }); }); it('creates only index template if it does not exist even if index exists', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate') { - return false; - } - - if (method === 'indices.exists') { - return true; - } - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: false }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); await sessionIndex.initialize(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(3); assertExistenceChecksPerformed(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.putTemplate', { + expect(mockElasticsearchClient.indices.putTemplate).toHaveBeenCalledWith({ name: indexTemplateName, body: getSessionIndexTemplate(indexName), }); }); it('creates only index if it does not exist even if index template exists', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate') { - return true; - } - - if (method === 'indices.exists') { - return false; - } - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: false }) + ); await sessionIndex.initialize(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(3); assertExistenceChecksPerformed(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { + expect(mockElasticsearchClient.indices.create).toHaveBeenCalledWith({ index: getSessionIndexTemplate(indexName).index_patterns, }); }); it('does not fail if tries to create index when it exists already', async () => { - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'indices.existsTemplate') { - return true; - } - - if (method === 'indices.exists') { - return false; - } - - if (method === 'indices.create') { - // eslint-disable-next-line no-throw-literal - throw { body: { error: { type: 'resource_already_exists_exception' } } }; - } - }); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValue( + securityMock.createApiResponse({ body: true }) + ); + mockElasticsearchClient.indices.exists.mockResolvedValue( + securityMock.createApiResponse({ body: false }) + ); + mockElasticsearchClient.indices.create.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ + body: { error: { type: 'resource_already_exists_exception' } }, + }) + ) + ); await sessionIndex.initialize(); }); it('works properly after failure', async () => { - const unexpectedError = new Error('Uh! Oh!'); - mockClusterClient.callAsInternalUser.mockImplementationOnce(() => - Promise.reject(unexpectedError) + const unexpectedError = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.indices.existsTemplate.mockRejectedValueOnce(unexpectedError); + mockElasticsearchClient.indices.existsTemplate.mockResolvedValueOnce( + securityMock.createApiResponse({ body: true }) ); - mockClusterClient.callAsInternalUser.mockImplementationOnce(() => Promise.resolve(true)); await expect(sessionIndex.initialize()).rejects.toBe(unexpectedError); await expect(sessionIndex.initialize()).resolves.toBe(undefined); @@ -171,13 +163,17 @@ describe('Session index', () => { describe('cleanUp', () => { const now = 123456; beforeEach(() => { - mockClusterClient.callAsInternalUser.mockResolvedValue({}); + mockElasticsearchClient.deleteByQuery.mockResolvedValue( + securityMock.createApiResponse({ body: {} }) + ); jest.spyOn(Date, 'now').mockImplementation(() => now); }); it('throws if call to Elasticsearch fails', async () => { - const failureReason = new Error('Uh oh.'); - mockClusterClient.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.deleteByQuery.mockRejectedValue(failureReason); await expect(sessionIndex.cleanUp()).rejects.toBe(failureReason); }); @@ -185,53 +181,55 @@ describe('Session index', () => { it('when neither `lifespan` nor `idleTimeout` is configured', async () => { await sessionIndex.cleanUp(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', { - index: indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { - query: { - bool: { - should: [ - // All expired sessions based on the lifespan, no matter which provider they belong to. - { range: { lifespanExpiration: { lte: now } } }, - // All sessions that belong to the providers that aren't configured. - { - bool: { - must_not: { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith( + { + index: indexName, + refresh: true, + body: { + query: { + bool: { + should: [ + // All expired sessions based on the lifespan, no matter which provider they belong to. + { range: { lifespanExpiration: { lte: now } } }, + // All sessions that belong to the providers that aren't configured. + { + bool: { + must_not: { + bool: { + should: [ + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + }, }, - }, - ], - minimum_should_match: 1, + ], + minimum_should_match: 1, + }, }, }, }, - }, - // The sessions that belong to a particular provider that are expired based on the idle timeout. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - should: [{ range: { idleTimeoutExpiration: { lte: now } } }], - minimum_should_match: 1, + // The sessions that belong to a particular provider that are expired based on the idle timeout. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + should: [{ range: { idleTimeoutExpiration: { lte: now } } }], + minimum_should_match: 1, + }, }, - }, - ], + ], + }, }, }, }, - }); + { ignore: [409, 404] } + ); }); it('when only `lifespan` is configured', async () => { @@ -243,68 +241,70 @@ describe('Session index', () => { loggingSystemMock.createLogger(), { isTLSEnabled: false } ), - clusterClient: mockClusterClient, + elasticsearchClient: mockElasticsearchClient, }); await sessionIndex.cleanUp(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', { - index: indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { - query: { - bool: { - should: [ - // All expired sessions based on the lifespan, no matter which provider they belong to. - { range: { lifespanExpiration: { lte: now } } }, - // All sessions that belong to the providers that aren't configured. - { - bool: { - must_not: { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith( + { + index: indexName, + refresh: true, + body: { + query: { + bool: { + should: [ + // All expired sessions based on the lifespan, no matter which provider they belong to. + { range: { lifespanExpiration: { lte: now } } }, + // All sessions that belong to the providers that aren't configured. + { + bool: { + must_not: { + bool: { + should: [ + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + }, }, - }, - ], - minimum_should_match: 1, + ], + minimum_should_match: 1, + }, }, }, }, - }, - // The sessions that belong to a particular provider but don't have a configured lifespan. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - must_not: { exists: { field: 'lifespanExpiration' } }, + // The sessions that belong to a particular provider but don't have a configured lifespan. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + must_not: { exists: { field: 'lifespanExpiration' } }, + }, }, - }, - // The sessions that belong to a particular provider that are expired based on the idle timeout. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - should: [{ range: { idleTimeoutExpiration: { lte: now } } }], - minimum_should_match: 1, + // The sessions that belong to a particular provider that are expired based on the idle timeout. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + should: [{ range: { idleTimeoutExpiration: { lte: now } } }], + minimum_should_match: 1, + }, }, - }, - ], + ], + }, }, }, }, - }); + { ignore: [409, 404] } + ); }); it('when only `idleTimeout` is configured', async () => { @@ -317,62 +317,64 @@ describe('Session index', () => { loggingSystemMock.createLogger(), { isTLSEnabled: false } ), - clusterClient: mockClusterClient, + elasticsearchClient: mockElasticsearchClient, }); await sessionIndex.cleanUp(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', { - index: indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { - query: { - bool: { - should: [ - // All expired sessions based on the lifespan, no matter which provider they belong to. - { range: { lifespanExpiration: { lte: now } } }, - // All sessions that belong to the providers that aren't configured. - { - bool: { - must_not: { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith( + { + index: indexName, + refresh: true, + body: { + query: { + bool: { + should: [ + // All expired sessions based on the lifespan, no matter which provider they belong to. + { range: { lifespanExpiration: { lte: now } } }, + // All sessions that belong to the providers that aren't configured. + { + bool: { + must_not: { + bool: { + should: [ + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + }, }, - }, - ], - minimum_should_match: 1, + ], + minimum_should_match: 1, + }, }, }, }, - }, - // The sessions that belong to a particular provider that are either expired based on the idle timeout - // or don't have it configured at all. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - should: [ - { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } }, - { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, - ], - minimum_should_match: 1, + // The sessions that belong to a particular provider that are either expired based on the idle timeout + // or don't have it configured at all. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + should: [ + { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } }, + { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, + ], + minimum_should_match: 1, + }, }, - }, - ], + ], + }, }, }, }, - }); + { ignore: [409, 404] } + ); }); it('when both `lifespan` and `idleTimeout` are configured', async () => { @@ -385,72 +387,74 @@ describe('Session index', () => { loggingSystemMock.createLogger(), { isTLSEnabled: false } ), - clusterClient: mockClusterClient, + elasticsearchClient: mockElasticsearchClient, }); await sessionIndex.cleanUp(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', { - index: indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { - query: { - bool: { - should: [ - // All expired sessions based on the lifespan, no matter which provider they belong to. - { range: { lifespanExpiration: { lte: now } } }, - // All sessions that belong to the providers that aren't configured. - { - bool: { - must_not: { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith( + { + index: indexName, + refresh: true, + body: { + query: { + bool: { + should: [ + // All expired sessions based on the lifespan, no matter which provider they belong to. + { range: { lifespanExpiration: { lte: now } } }, + // All sessions that belong to the providers that aren't configured. + { + bool: { + must_not: { + bool: { + should: [ + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + }, }, - }, - ], - minimum_should_match: 1, + ], + minimum_should_match: 1, + }, }, }, }, - }, - // The sessions that belong to a particular provider but don't have a configured lifespan. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - must_not: { exists: { field: 'lifespanExpiration' } }, + // The sessions that belong to a particular provider but don't have a configured lifespan. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + must_not: { exists: { field: 'lifespanExpiration' } }, + }, }, - }, - // The sessions that belong to a particular provider that are either expired based on the idle timeout - // or don't have it configured at all. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic' } }, - ], - should: [ - { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } }, - { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, - ], - minimum_should_match: 1, + // The sessions that belong to a particular provider that are either expired based on the idle timeout + // or don't have it configured at all. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic' } }, + ], + should: [ + { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } }, + { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, + ], + minimum_should_match: 1, + }, }, - }, - ], + ], + }, }, }, }, - }); + { ignore: [409, 404] } + ); }); it('when both `lifespan` and `idleTimeout` are configured and multiple providers are enabled', async () => { @@ -478,127 +482,132 @@ describe('Session index', () => { loggingSystemMock.createLogger(), { isTLSEnabled: false } ), - clusterClient: mockClusterClient, + elasticsearchClient: mockElasticsearchClient, }); await sessionIndex.cleanUp(); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', { - index: indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { - query: { - bool: { - should: [ - // All expired sessions based on the lifespan, no matter which provider they belong to. - { range: { lifespanExpiration: { lte: now } } }, - // All sessions that belong to the providers that aren't configured. - { - bool: { - must_not: { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic1' } }, - ], + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith( + { + index: indexName, + refresh: true, + body: { + query: { + bool: { + should: [ + // All expired sessions based on the lifespan, no matter which provider they belong to. + { range: { lifespanExpiration: { lte: now } } }, + // All sessions that belong to the providers that aren't configured. + { + bool: { + must_not: { + bool: { + should: [ + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic1' } }, + ], + }, }, - }, - { - bool: { - must: [ - { term: { 'provider.type': 'saml' } }, - { term: { 'provider.name': 'saml1' } }, - ], + { + bool: { + must: [ + { term: { 'provider.type': 'saml' } }, + { term: { 'provider.name': 'saml1' } }, + ], + }, }, - }, - ], - minimum_should_match: 1, + ], + minimum_should_match: 1, + }, }, }, }, - }, - // The sessions that belong to a Basic provider but don't have a configured lifespan. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic1' } }, - ], - must_not: { exists: { field: 'lifespanExpiration' } }, + // The sessions that belong to a Basic provider but don't have a configured lifespan. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic1' } }, + ], + must_not: { exists: { field: 'lifespanExpiration' } }, + }, }, - }, - // The sessions that belong to a Basic provider that are either expired based on the idle timeout - // or don't have it configured at all. - { - bool: { - must: [ - { term: { 'provider.type': 'basic' } }, - { term: { 'provider.name': 'basic1' } }, - ], - should: [ - { range: { idleTimeoutExpiration: { lte: now - 3 * globalIdleTimeout } } }, - { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, - ], - minimum_should_match: 1, + // The sessions that belong to a Basic provider that are either expired based on the idle timeout + // or don't have it configured at all. + { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic1' } }, + ], + should: [ + { range: { idleTimeoutExpiration: { lte: now - 3 * globalIdleTimeout } } }, + { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, + ], + minimum_should_match: 1, + }, }, - }, - // The sessions that belong to a SAML provider but don't have a configured lifespan. - { - bool: { - must: [ - { term: { 'provider.type': 'saml' } }, - { term: { 'provider.name': 'saml1' } }, - ], - must_not: { exists: { field: 'lifespanExpiration' } }, + // The sessions that belong to a SAML provider but don't have a configured lifespan. + { + bool: { + must: [ + { term: { 'provider.type': 'saml' } }, + { term: { 'provider.name': 'saml1' } }, + ], + must_not: { exists: { field: 'lifespanExpiration' } }, + }, }, - }, - // The sessions that belong to a SAML provider that are either expired based on the idle timeout - // or don't have it configured at all. - { - bool: { - must: [ - { term: { 'provider.type': 'saml' } }, - { term: { 'provider.name': 'saml1' } }, - ], - should: [ - { range: { idleTimeoutExpiration: { lte: now - 3 * samlIdleTimeout } } }, - { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, - ], - minimum_should_match: 1, + // The sessions that belong to a SAML provider that are either expired based on the idle timeout + // or don't have it configured at all. + { + bool: { + must: [ + { term: { 'provider.type': 'saml' } }, + { term: { 'provider.name': 'saml1' } }, + ], + should: [ + { range: { idleTimeoutExpiration: { lte: now - 3 * samlIdleTimeout } } }, + { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }, + ], + minimum_should_match: 1, + }, }, - }, - ], + ], + }, }, }, }, - }); + { ignore: [409, 404] } + ); }); }); describe('#get', () => { it('throws if call to Elasticsearch fails', async () => { - const failureReason = new Error('Uh oh.'); - mockClusterClient.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.get.mockRejectedValue(failureReason); await expect(sessionIndex.get('some-sid')).rejects.toBe(failureReason); }); it('returns `null` if index is not found', async () => { - mockClusterClient.callAsInternalUser.mockResolvedValue({ status: 404 }); + mockElasticsearchClient.get.mockResolvedValue( + securityMock.createApiResponse({ statusCode: 404, body: { status: 404 } }) + ); await expect(sessionIndex.get('some-sid')).resolves.toBeNull(); }); it('returns `null` if session index value document is not found', async () => { - mockClusterClient.callAsInternalUser.mockResolvedValue({ - found: false, - status: 200, - }); + mockElasticsearchClient.get.mockResolvedValue( + securityMock.createApiResponse({ body: { status: 200, found: false } }) + ); await expect(sessionIndex.get('some-sid')).resolves.toBeNull(); }); @@ -612,13 +621,17 @@ describe('Session index', () => { content: 'some-encrypted-content', }; - mockClusterClient.callAsInternalUser.mockResolvedValue({ - found: true, - status: 200, - _source: indexDocumentSource, - _primary_term: 1, - _seq_no: 456, - }); + mockElasticsearchClient.get.mockResolvedValue( + securityMock.createApiResponse({ + body: { + found: true, + status: 200, + _source: indexDocumentSource, + _primary_term: 1, + _seq_no: 456, + }, + }) + ); await expect(sessionIndex.get('some-sid')).resolves.toEqual({ ...indexDocumentSource, @@ -626,19 +639,20 @@ describe('Session index', () => { metadata: { primaryTerm: 1, sequenceNumber: 456 }, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('get', { - id: 'some-sid', - ignore: [404], - index: indexName, - }); + expect(mockElasticsearchClient.get).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.get).toHaveBeenCalledWith( + { id: 'some-sid', index: indexName }, + { ignore: [404] } + ); }); }); describe('#create', () => { it('throws if call to Elasticsearch fails', async () => { - const failureReason = new Error('Uh oh.'); - mockClusterClient.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.create.mockRejectedValue(failureReason); await expect( sessionIndex.create({ @@ -653,10 +667,9 @@ describe('Session index', () => { }); it('properly stores session value in the index', async () => { - mockClusterClient.callAsInternalUser.mockResolvedValue({ - _primary_term: 321, - _seq_no: 654, - }); + mockElasticsearchClient.create.mockResolvedValue( + securityMock.createApiResponse({ body: { _primary_term: 321, _seq_no: 654 } }) + ); const sid = 'some-long-sid'; const sessionValue = { @@ -673,8 +686,8 @@ describe('Session index', () => { metadata: { primaryTerm: 321, sequenceNumber: 654 }, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('create', { + expect(mockElasticsearchClient.create).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.create).toHaveBeenCalledWith({ id: sid, index: indexName, body: sessionValue, @@ -685,8 +698,10 @@ describe('Session index', () => { describe('#update', () => { it('throws if call to Elasticsearch fails', async () => { - const failureReason = new Error('Uh oh.'); - mockClusterClient.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.index.mockRejectedValue(failureReason); await expect(sessionIndex.update(sessionIndexMock.createValue())).rejects.toBe(failureReason); }); @@ -700,21 +715,20 @@ describe('Session index', () => { content: 'some-updated-encrypted-content', }; - mockClusterClient.callAsInternalUser.mockImplementation(async (method) => { - if (method === 'get') { - return { + mockElasticsearchClient.get.mockResolvedValue( + securityMock.createApiResponse({ + body: { found: true, status: 200, _source: latestSessionValue, _primary_term: 321, _seq_no: 654, - }; - } - - if (method === 'index') { - return { status: 409 }; - } - }); + }, + }) + ); + mockElasticsearchClient.index.mockResolvedValue( + securityMock.createApiResponse({ statusCode: 409, body: { status: 409 } }) + ); const sid = 'some-long-sid'; const metadata = { primaryTerm: 123, sequenceNumber: 456 }; @@ -732,24 +746,23 @@ describe('Session index', () => { metadata: { primaryTerm: 321, sequenceNumber: 654 }, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('index', { - id: sid, - index: indexName, - body: sessionValue, - ifSeqNo: 456, - ifPrimaryTerm: 123, - refresh: 'wait_for', - ignore: [409], - }); + expect(mockElasticsearchClient.index).toHaveBeenCalledWith( + { + id: sid, + index: indexName, + body: sessionValue, + if_seq_no: 456, + if_primary_term: 123, + refresh: 'wait_for', + }, + { ignore: [409] } + ); }); it('properly stores session value in the index', async () => { - mockClusterClient.callAsInternalUser.mockResolvedValue({ - _primary_term: 321, - _seq_no: 654, - status: 200, - }); + mockElasticsearchClient.index.mockResolvedValue( + securityMock.createApiResponse({ body: { _primary_term: 321, _seq_no: 654, status: 200 } }) + ); const sid = 'some-long-sid'; const metadata = { primaryTerm: 123, sequenceNumber: 456 }; @@ -767,23 +780,27 @@ describe('Session index', () => { metadata: { primaryTerm: 321, sequenceNumber: 654 }, }); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('index', { - id: sid, - index: indexName, - body: sessionValue, - ifSeqNo: 456, - ifPrimaryTerm: 123, - refresh: 'wait_for', - ignore: [409], - }); + expect(mockElasticsearchClient.index).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.index).toHaveBeenCalledWith( + { + id: sid, + index: indexName, + body: sessionValue, + if_seq_no: 456, + if_primary_term: 123, + refresh: 'wait_for', + }, + { ignore: [409] } + ); }); }); describe('#clear', () => { it('throws if call to Elasticsearch fails', async () => { - const failureReason = new Error('Uh oh.'); - mockClusterClient.callAsInternalUser.mockRejectedValue(failureReason); + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.delete.mockRejectedValue(failureReason); await expect(sessionIndex.clear('some-long-sid')).rejects.toBe(failureReason); }); @@ -791,13 +808,11 @@ describe('Session index', () => { it('properly removes session value from the index', async () => { await sessionIndex.clear('some-long-sid'); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('delete', { - id: 'some-long-sid', - index: indexName, - refresh: 'wait_for', - ignore: [404], - }); + expect(mockElasticsearchClient.delete).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.delete).toHaveBeenCalledWith( + { id: 'some-long-sid', index: indexName, refresh: 'wait_for' }, + { ignore: [404] } + ); }); }); }); diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 45b2f4489c195..13250531d391e 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import type { ILegacyClusterClient, Logger } from '../../../../../src/core/server'; +import type { ElasticsearchClient, Logger } from '../../../../../src/core/server'; import type { AuthenticationProvider } from '../../common/model'; import type { ConfigType } from '../config'; export interface SessionIndexOptions { - readonly clusterClient: ILegacyClusterClient; + readonly elasticsearchClient: ElasticsearchClient; readonly kibanaIndexName: string; readonly config: Pick; readonly logger: Logger; @@ -137,11 +137,10 @@ export class SessionIndex { */ async get(sid: string) { try { - const response = await this.options.clusterClient.callAsInternalUser('get', { - id: sid, - ignore: [404], - index: this.indexName, - }); + const { body: response } = await this.options.elasticsearchClient.get( + { id: sid, index: this.indexName }, + { ignore: [404] } + ); const docNotFound = response.found === false; const indexNotFound = response.status === 404; @@ -176,9 +175,8 @@ export class SessionIndex { const { sid, ...sessionValueToStore } = sessionValue; try { const { - _primary_term: primaryTerm, - _seq_no: sequenceNumber, - } = await this.options.clusterClient.callAsInternalUser('create', { + body: { _primary_term: primaryTerm, _seq_no: sequenceNumber }, + } = await this.options.elasticsearchClient.create({ id: sid, // We cannot control whether index is created automatically during this operation or not. // But we can reduce probability of getting into a weird state when session is being created @@ -203,15 +201,17 @@ export class SessionIndex { async update(sessionValue: Readonly) { const { sid, metadata, ...sessionValueToStore } = sessionValue; try { - const response = await this.options.clusterClient.callAsInternalUser('index', { - id: sid, - index: this.indexName, - body: sessionValueToStore, - ifSeqNo: metadata.sequenceNumber, - ifPrimaryTerm: metadata.primaryTerm, - refresh: 'wait_for', - ignore: [409], - }); + const { body: response } = await this.options.elasticsearchClient.index( + { + id: sid, + index: this.indexName, + body: sessionValueToStore, + if_seq_no: metadata.sequenceNumber, + if_primary_term: metadata.primaryTerm, + refresh: 'wait_for', + }, + { ignore: [409] } + ); // We don't want to override changes that were made after we fetched session value or // re-create it if has been deleted already. If we detect such a case we discard changes and @@ -242,12 +242,10 @@ export class SessionIndex { try { // We don't specify primary term and sequence number as delete should always take precedence // over any updates that could happen in the meantime. - await this.options.clusterClient.callAsInternalUser('delete', { - id: sid, - index: this.indexName, - refresh: 'wait_for', - ignore: [404], - }); + await this.options.elasticsearchClient.delete( + { id: sid, index: this.indexName, refresh: 'wait_for' }, + { ignore: [404] } + ); } catch (err) { this.options.logger.error(`Failed to clear session value: ${err.message}`); throw err; @@ -267,10 +265,11 @@ export class SessionIndex { // Check if required index template exists. let indexTemplateExists = false; try { - indexTemplateExists = await this.options.clusterClient.callAsInternalUser( - 'indices.existsTemplate', - { name: sessionIndexTemplateName } - ); + indexTemplateExists = ( + await this.options.elasticsearchClient.indices.existsTemplate({ + name: sessionIndexTemplateName, + }) + ).body; } catch (err) { this.options.logger.error( `Failed to check if session index template exists: ${err.message}` @@ -283,7 +282,7 @@ export class SessionIndex { this.options.logger.debug('Session index template already exists.'); } else { try { - await this.options.clusterClient.callAsInternalUser('indices.putTemplate', { + await this.options.elasticsearchClient.indices.putTemplate({ name: sessionIndexTemplateName, body: getSessionIndexTemplate(this.indexName), }); @@ -298,9 +297,9 @@ export class SessionIndex { // always enabled, so we create session index explicitly. let indexExists = false; try { - indexExists = await this.options.clusterClient.callAsInternalUser('indices.exists', { - index: this.indexName, - }); + indexExists = ( + await this.options.elasticsearchClient.indices.exists({ index: this.indexName }) + ).body; } catch (err) { this.options.logger.error(`Failed to check if session index exists: ${err.message}`); return reject(err); @@ -311,9 +310,7 @@ export class SessionIndex { this.options.logger.debug('Session index already exists.'); } else { try { - await this.options.clusterClient.callAsInternalUser('indices.create', { - index: this.indexName, - }); + await this.options.elasticsearchClient.indices.create({ index: this.indexName }); this.options.logger.debug('Successfully created session index.'); } catch (err) { // There can be a race condition if index is created by another Kibana instance. @@ -399,12 +396,14 @@ export class SessionIndex { } try { - const response = await this.options.clusterClient.callAsInternalUser('deleteByQuery', { - index: this.indexName, - refresh: 'wait_for', - ignore: [409, 404], - body: { query: { bool: { should: deleteQueries } } }, - }); + const { body: response } = await this.options.elasticsearchClient.deleteByQuery( + { + index: this.indexName, + refresh: true, + body: { query: { bool: { should: deleteQueries } } }, + }, + { ignore: [409, 404] } + ); if (response.deleted > 0) { this.options.logger.debug( diff --git a/x-pack/plugins/security/server/session_management/session_management_service.test.ts b/x-pack/plugins/security/server/session_management/session_management_service.test.ts index 08d4c491d1556..d3e5c876c9974 100644 --- a/x-pack/plugins/security/server/session_management/session_management_service.test.ts +++ b/x-pack/plugins/security/server/session_management/session_management_service.test.ts @@ -21,7 +21,7 @@ import { loggingSystemMock, } from '../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../task_manager/server/mocks'; -import { TaskManagerStartContract } from '../../../task_manager/server'; +import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../task_manager/server'; describe('SessionManagementService', () => { let service: SessionManagementService; @@ -30,21 +30,19 @@ describe('SessionManagementService', () => { }); describe('setup()', () => { - it('exposes proper contract', () => { + it('registers cleanup task', () => { const mockCoreSetup = coreMock.createSetup(); const mockTaskManager = taskManagerMock.createSetup(); expect( service.setup({ - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), http: mockCoreSetup.http, config: createConfig(ConfigSchema.validate({}), loggingSystemMock.createLogger(), { isTLSEnabled: false, }), - kibanaIndexName: '.kibana', taskManager: mockTaskManager, }) - ).toEqual({ session: expect.any(Session) }); + ).toBeUndefined(); expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledWith({ @@ -54,60 +52,35 @@ describe('SessionManagementService', () => { }, }); }); - - it('registers proper session index cleanup task runner', () => { - const mockSessionIndexCleanUp = jest.spyOn(SessionIndex.prototype, 'cleanUp'); - const mockTaskManager = taskManagerMock.createSetup(); - - const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); - mockClusterClient.callAsInternalUser.mockResolvedValue({}); - service.setup({ - clusterClient: mockClusterClient, - http: coreMock.createSetup().http, - config: createConfig(ConfigSchema.validate({}), loggingSystemMock.createLogger(), { - isTLSEnabled: false, - }), - kibanaIndexName: '.kibana', - taskManager: mockTaskManager, - }); - - const [ - [ - { - [SESSION_INDEX_CLEANUP_TASK_NAME]: { createTaskRunner }, - }, - ], - ] = mockTaskManager.registerTaskDefinitions.mock.calls; - expect(mockSessionIndexCleanUp).not.toHaveBeenCalled(); - - const runner = createTaskRunner({} as any); - runner.run(); - expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(1); - - runner.run(); - expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(2); - }); }); describe('start()', () => { let mockSessionIndexInitialize: jest.SpyInstance; let mockTaskManager: jest.Mocked; + let sessionCleanupTaskRunCreator: TaskRunCreatorFunction; beforeEach(() => { mockSessionIndexInitialize = jest.spyOn(SessionIndex.prototype, 'initialize'); mockTaskManager = taskManagerMock.createStart(); mockTaskManager.ensureScheduled.mockResolvedValue(undefined as any); - const mockCoreSetup = coreMock.createSetup(); + const mockTaskManagerSetup = taskManagerMock.createSetup(); service.setup({ - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), - http: mockCoreSetup.http, + http: coreMock.createSetup().http, config: createConfig(ConfigSchema.validate({}), loggingSystemMock.createLogger(), { isTLSEnabled: false, }), - kibanaIndexName: '.kibana', - taskManager: taskManagerMock.createSetup(), + taskManager: mockTaskManagerSetup, }); + + const [ + [ + { + [SESSION_INDEX_CLEANUP_TASK_NAME]: { createTaskRunner }, + }, + ], + ] = mockTaskManagerSetup.registerTaskDefinitions.mock.calls; + sessionCleanupTaskRunCreator = createTaskRunner; }); afterEach(() => { @@ -117,13 +90,43 @@ describe('SessionManagementService', () => { it('exposes proper contract', () => { const mockStatusSubject = new Subject(); expect( - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }) - ).toBeUndefined(); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }) + ).toEqual({ session: expect.any(Session) }); + }); + + it('registers proper session index cleanup task runner', () => { + const mockSessionIndexCleanUp = jest.spyOn(SessionIndex.prototype, 'cleanUp'); + const mockStatusSubject = new Subject(); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); + + expect(mockSessionIndexCleanUp).not.toHaveBeenCalled(); + + const runner = sessionCleanupTaskRunCreator({} as any); + runner.run(); + expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(1); + + runner.run(); + expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(2); }); it('initializes session index and schedules session index cleanup task when Elasticsearch goes online', async () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); // ES isn't online yet. expect(mockSessionIndexInitialize).not.toHaveBeenCalled(); @@ -155,7 +158,12 @@ describe('SessionManagementService', () => { it('removes old cleanup task if cleanup interval changes', async () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); mockTaskManager.get.mockResolvedValue({ schedule: { interval: '2000s' } } as any); @@ -185,7 +193,12 @@ describe('SessionManagementService', () => { it('does not remove old cleanup task if cleanup interval does not change', async () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); mockTaskManager.get.mockResolvedValue({ schedule: { interval: '3600s' } } as any); @@ -206,7 +219,12 @@ describe('SessionManagementService', () => { it('schedules retry if index initialization fails', async () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); mockSessionIndexInitialize.mockRejectedValue(new Error('ugh :/')); @@ -237,7 +255,12 @@ describe('SessionManagementService', () => { it('schedules retry if cleanup task registration fails', async () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); mockTaskManager.ensureScheduled.mockRejectedValue(new Error('ugh :/')); @@ -277,12 +300,10 @@ describe('SessionManagementService', () => { const mockCoreSetup = coreMock.createSetup(); service.setup({ - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), http: mockCoreSetup.http, config: createConfig(ConfigSchema.validate({}), loggingSystemMock.createLogger(), { isTLSEnabled: false, }), - kibanaIndexName: '.kibana', taskManager: taskManagerMock.createSetup(), }); }); @@ -293,7 +314,12 @@ describe('SessionManagementService', () => { it('properly unsubscribes from status updates', () => { const mockStatusSubject = new Subject(); - service.start({ online$: mockStatusSubject.asObservable(), taskManager: mockTaskManager }); + service.start({ + elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(), + kibanaIndexName: '.kibana', + online$: mockStatusSubject.asObservable(), + taskManager: mockTaskManager, + }); service.stop(); diff --git a/x-pack/plugins/security/server/session_management/session_management_service.ts b/x-pack/plugins/security/server/session_management/session_management_service.ts index fc2e85d683d58..6bd9d8cb3a8fe 100644 --- a/x-pack/plugins/security/server/session_management/session_management_service.ts +++ b/x-pack/plugins/security/server/session_management/session_management_service.ts @@ -6,8 +6,8 @@ import { Observable, Subscription } from 'rxjs'; import { + ElasticsearchClient, HttpServiceSetup, - ILegacyClusterClient, Logger, SavedObjectsErrorHelpers, } from '../../../../../src/core/server'; @@ -21,17 +21,17 @@ import { Session } from './session'; export interface SessionManagementServiceSetupParams { readonly http: Pick; readonly config: ConfigType; - readonly clusterClient: ILegacyClusterClient; - readonly kibanaIndexName: string; readonly taskManager: TaskManagerSetupContract; } export interface SessionManagementServiceStartParams { + readonly elasticsearchClient: ElasticsearchClient; + readonly kibanaIndexName: string; readonly online$: Observable; readonly taskManager: TaskManagerStartContract; } -export interface SessionManagementServiceSetup { +export interface SessionManagementServiceStart { readonly session: Session; } @@ -46,34 +46,22 @@ export const SESSION_INDEX_CLEANUP_TASK_NAME = 'session_cleanup'; export class SessionManagementService { private statusSubscription?: Subscription; private sessionIndex!: SessionIndex; + private sessionCookie!: SessionCookie; private config!: ConfigType; private isCleanupTaskScheduled = false; constructor(private readonly logger: Logger) {} - setup({ - config, - clusterClient, - http, - kibanaIndexName, - taskManager, - }: SessionManagementServiceSetupParams): SessionManagementServiceSetup { + setup({ config, http, taskManager }: SessionManagementServiceSetupParams) { this.config = config; - const sessionCookie = new SessionCookie({ + this.sessionCookie = new SessionCookie({ config, createCookieSessionStorageFactory: http.createCookieSessionStorageFactory, serverBasePath: http.basePath.serverBasePath || '/', logger: this.logger.get('cookie'), }); - this.sessionIndex = new SessionIndex({ - config, - clusterClient, - kibanaIndexName, - logger: this.logger.get('index'), - }); - // Register task that will perform periodic session index cleanup. taskManager.registerTaskDefinitions({ [SESSION_INDEX_CLEANUP_TASK_NAME]: { @@ -81,18 +69,21 @@ export class SessionManagementService { createTaskRunner: () => ({ run: () => this.sessionIndex.cleanUp() }), }, }); - - return { - session: new Session({ - logger: this.logger, - sessionCookie, - sessionIndex: this.sessionIndex, - config, - }), - }; } - start({ online$, taskManager }: SessionManagementServiceStartParams) { + start({ + elasticsearchClient, + kibanaIndexName, + online$, + taskManager, + }: SessionManagementServiceStartParams): SessionManagementServiceStart { + this.sessionIndex = new SessionIndex({ + config: this.config, + elasticsearchClient, + kibanaIndexName, + logger: this.logger.get('index'), + }); + this.statusSubscription = online$.subscribe(async ({ scheduleRetry }) => { try { await Promise.all([this.sessionIndex.initialize(), this.scheduleCleanupTask(taskManager)]); @@ -100,6 +91,15 @@ export class SessionManagementService { scheduleRetry(); } }); + + return { + session: new Session({ + logger: this.logger, + sessionCookie: this.sessionCookie, + sessionIndex: this.sessionIndex, + config: this.config, + }), + }; } stop() { From cacce7a866f8eeb8b878c4427d344f34d1517460 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 21 Jan 2021 14:51:29 -0700 Subject: [PATCH 05/60] [ftr/verbose_instance] check for `.finally()` before using it (#88998) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/providers/verbose_instance.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts index 248b55d85d8f5..cc2ecad82fb19 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts @@ -65,7 +65,11 @@ export function createVerboseInstance( } const { returned } = result; - if (returned && typeof returned.then === 'function') { + if ( + returned && + typeof returned.then === 'function' && + typeof returned.finally === 'function' + ) { return returned.finally(() => { log.indent(-2); }); From c495093f76e3ee899e9298970811b82e0627745f Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 21 Jan 2021 14:39:14 -0800 Subject: [PATCH 06/60] [App Search] Move generateEnginePath out from EngineLogic values to its own helper (#89022) * [Feedback] Move generateEnginePath to its own standalone helper - instead of living inside EngineLogic.values - I forgot Kea lets us do this now! * Update all components using generateEngineRouter to import helper directly --- .../app_search/__mocks__/engine_logic.mock.ts | 12 ++++---- .../analytics/analytics_router.test.tsx | 4 +-- .../components/analytics/analytics_router.tsx | 5 +--- .../document_creation_buttons.test.tsx | 5 ++-- .../document_creation_buttons.tsx | 5 ++-- .../documents/document_detail_logic.ts | 6 ++-- .../components/engine/engine_logic.test.ts | 23 --------------- .../components/engine/engine_logic.ts | 11 -------- .../components/engine/engine_nav.tsx | 3 +- .../app_search/components/engine/index.ts | 1 + .../components/engine/utils.test.ts | 28 +++++++++++++++++++ .../app_search/components/engine/utils.ts | 17 +++++++++++ .../components/recent_api_logs.test.tsx | 4 +-- .../components/recent_api_logs.tsx | 5 +--- .../components/total_charts.test.tsx | 3 +- .../components/total_charts.tsx | 3 +- 16 files changed, 65 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts index 5c327f64d7775..6326a41c1d2ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts @@ -8,16 +8,14 @@ import { generatePath } from 'react-router-dom'; export const mockEngineValues = { engineName: 'some-engine', - // Note: using getters allows us to use `this`, which lets tests - // override engineName and still generate correct engine names - get generateEnginePath() { - return jest.fn((path, pathParams = {}) => - generatePath(path, { engineName: this.engineName, ...pathParams }) - ); - }, engine: {}, }; +export const mockGenerateEnginePath = jest.fn((path, pathParams = {}) => + generatePath(path, { engineName: mockEngineValues.engineName, ...pathParams }) +); + jest.mock('../components/engine', () => ({ EngineLogic: { values: mockEngineValues }, + generateEnginePath: mockGenerateEnginePath, })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx index aea107a137da1..2cc6ff32d0ad9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setMockValues } from '../../../__mocks__'; -import { mockEngineValues } from '../../__mocks__'; +import '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; @@ -16,7 +15,6 @@ import { AnalyticsRouter } from './'; describe('AnalyticsRouter', () => { // Detailed route testing is better done via E2E tests it('renders', () => { - setMockValues(mockEngineValues); const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx index 60c0f2a3fd3e8..f549a1a8d9091 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { useValues } from 'kea'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -22,7 +21,7 @@ import { ENGINE_ANALYTICS_QUERY_DETAILS_PATH, ENGINE_ANALYTICS_QUERY_DETAIL_PATH, } from '../../routes'; -import { EngineLogic } from '../engine'; +import { generateEnginePath } from '../engine'; import { ANALYTICS_TITLE, @@ -46,8 +45,6 @@ interface Props { engineBreadcrumb: BreadcrumbTrail; } export const AnalyticsRouter: React.FC = ({ engineBreadcrumb }) => { - const { generateEnginePath } = useValues(EngineLogic); - const ANALYTICS_BREADCRUMB = [...engineBreadcrumb, ANALYTICS_TITLE]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx index d8684355c1a81..bd4d088bc1d8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; -import { mockEngineValues } from '../../__mocks__'; +import { setMockActions } from '../../../__mocks__/kea.mock'; +import '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; @@ -21,7 +21,6 @@ describe('DocumentCreationButtons', () => { beforeEach(() => { jest.clearAllMocks(); - setMockValues(mockEngineValues); setMockActions(actions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 93c93224b5982..3a53b3c83d9eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { useActions, useValues } from 'kea'; +import { useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -22,7 +22,7 @@ import { import { EuiCardTo } from '../../../shared/react_router_helpers'; import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; -import { EngineLogic } from '../engine'; +import { generateEnginePath } from '../engine'; import { DocumentCreationLogic } from './'; @@ -33,7 +33,6 @@ interface Props { export const DocumentCreationButtons: React.FC = ({ disabled = false }) => { const { openDocumentCreation } = useActions(DocumentCreationLogic); - const { generateEnginePath } = useValues(EngineLogic); const crawlerLink = generateEnginePath(ENGINE_CRAWLER_PATH); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts index b8d67ac56b3a2..8141ba73d418e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -12,7 +12,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import { HttpLogic } from '../../../shared/http'; import { ENGINE_DOCUMENTS_PATH } from '../../routes'; -import { EngineLogic } from '../engine'; +import { EngineLogic, generateEnginePath } from '../engine'; import { FieldDetails } from './types'; @@ -52,7 +52,7 @@ export const DocumentDetailLogic = kea({ }), listeners: ({ actions }) => ({ getDocumentDetails: async ({ documentId }) => { - const { engineName, generateEnginePath } = EngineLogic.values; + const { engineName } = EngineLogic.values; const { navigateToUrl } = KibanaLogic.values; try { @@ -70,7 +70,7 @@ export const DocumentDetailLogic = kea({ } }, deleteDocument: async ({ documentId }) => { - const { engineName, generateEnginePath } = EngineLogic.values; + const { engineName } = EngineLogic.values; const { navigateToUrl } = KibanaLogic.values; const CONFIRM_DELETE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 32c3382cf187a..48cbaeef70c1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -36,7 +36,6 @@ describe('EngineLogic', () => { dataLoading: true, engine: {}, engineName: '', - generateEnginePath: expect.any(Function), isMetaEngine: false, isSampleEngine: false, hasSchemaConflicts: false, @@ -198,28 +197,6 @@ describe('EngineLogic', () => { }); describe('selectors', () => { - describe('generateEnginePath', () => { - it('returns helper function that generates paths with engineName prefilled', () => { - mount({ engineName: 'hello-world' }); - - const generatedPath = EngineLogic.values.generateEnginePath('/engines/:engineName/example'); - expect(generatedPath).toEqual('/engines/hello-world/example'); - }); - - it('allows overriding engineName and filling other params', () => { - mount({ engineName: 'lorem-ipsum' }); - - const generatedPath = EngineLogic.values.generateEnginePath( - '/engines/:engineName/foo/:bar', - { - engineName: 'dolor-sit', - bar: 'baz', - } - ); - expect(generatedPath).toEqual('/engines/dolor-sit/foo/baz'); - }); - }); - describe('isSampleEngine', () => { it('should be set based on engine.sample', () => { const mockSampleEngine = { ...mockEngineData, sample: true }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index 04d06b596080a..9f3fe721b74de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -5,7 +5,6 @@ */ import { kea, MakeLogicType } from 'kea'; -import { generatePath } from 'react-router-dom'; import { HttpLogic } from '../../../shared/http'; @@ -16,7 +15,6 @@ interface EngineValues { dataLoading: boolean; engine: Partial; engineName: string; - generateEnginePath: Function; isMetaEngine: boolean; isSampleEngine: boolean; hasSchemaConflicts: boolean; @@ -78,15 +76,6 @@ export const EngineLogic = kea>({ ], }, selectors: ({ selectors }) => ({ - generateEnginePath: [ - () => [selectors.engineName], - (engineName) => { - const generateEnginePath = (path: string, pathParams: object = {}) => { - return generatePath(path, { engineName, ...pathParams }); - }; - return generateEnginePath; - }, - ], isMetaEngine: [() => [selectors.engine], (engine) => engine?.type === 'meta'], isSampleEngine: [() => [selectors.engine], (engine) => !!engine?.sample], hasSchemaConflicts: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index fd30e04d34932..0e5a7d56e9065 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -40,7 +40,7 @@ import { RESULT_SETTINGS_TITLE } from '../result_settings'; import { SEARCH_UI_TITLE } from '../search_ui'; import { API_LOGS_TITLE } from '../api_logs'; -import { EngineLogic } from './'; +import { EngineLogic, generateEnginePath } from './'; import { EngineDetails } from './types'; import './engine_nav.scss'; @@ -64,7 +64,6 @@ export const EngineNav: React.FC = () => { const { engineName, - generateEnginePath, dataLoading, isSampleEngine, isMetaEngine, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts index 4e7d81f73fb8d..7846eb9d03b71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts @@ -7,3 +7,4 @@ export { EngineRouter } from './engine_router'; export { EngineNav } from './engine_nav'; export { EngineLogic } from './engine_logic'; +export { generateEnginePath } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.test.ts new file mode 100644 index 0000000000000..cff4065c13f5e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockEngineValues } from '../../__mocks__'; + +import { generateEnginePath } from './utils'; + +describe('generateEnginePath', () => { + mockEngineValues.engineName = 'hello-world'; + + it('generates paths with engineName filled from state', () => { + expect(generateEnginePath('/engines/:engineName/example')).toEqual( + '/engines/hello-world/example' + ); + }); + + it('allows overriding engineName and filling other params', () => { + expect( + generateEnginePath('/engines/:engineName/foo/:bar', { + engineName: 'override', + bar: 'baz', + }) + ).toEqual('/engines/override/foo/baz'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.ts new file mode 100644 index 0000000000000..b7efcbb6e6b27 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { generatePath } from 'react-router-dom'; + +import { EngineLogic } from './'; + +/** + * Generate a path with engineName automatically filled from EngineLogic state + */ +export const generateEnginePath = (path: string, pathParams: object = {}) => { + const { engineName } = EngineLogic.values; + return generatePath(path, { engineName, ...pathParams }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx index 9da63ca639bbf..d7d22cafee432 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { mockEngineValues } from '../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; @@ -19,7 +18,6 @@ describe('RecentApiLogs', () => { beforeAll(() => { jest.clearAllMocks(); - setMockValues(mockEngineValues); wrapper = shallow(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index 19c931cefc1e3..207666ef67466 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { useValues } from 'kea'; import { EuiPageContent, @@ -17,14 +16,12 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; -import { EngineLogic } from '../../engine'; +import { generateEnginePath } from '../../engine'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { VIEW_API_LOGS } from '../constants'; export const RecentApiLogs: React.FC = () => { - const { generateEnginePath } = useValues(EngineLogic); - return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx index 98718dea7130f..14fb19b8ca2be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx @@ -5,7 +5,7 @@ */ import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { mockEngineValues } from '../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; @@ -21,7 +21,6 @@ describe('TotalCharts', () => { beforeAll(() => { jest.clearAllMocks(); setMockValues({ - ...mockEngineValues, startDate: '1970-01-01', queriesPerDay: [0, 1, 2, 3, 5, 10, 50], operationsPerDay: [0, 0, 0, 0, 0, 0, 0], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx index 02453cc8a150f..e8454cdc95ebc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -20,7 +20,7 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH } from '../../../routes'; -import { EngineLogic } from '../../engine'; +import { generateEnginePath } from '../../engine'; import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; @@ -28,7 +28,6 @@ import { AnalyticsChart, convertToChartData } from '../../analytics'; import { EngineOverviewLogic } from '../'; export const TotalCharts: React.FC = () => { - const { generateEnginePath } = useValues(EngineLogic); const { startDate, queriesPerDay, operationsPerDay } = useValues(EngineOverviewLogic); return ( From 4281a347c6b3bb1304c8cf70ba82a34c466b2ae5 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 21 Jan 2021 17:32:18 -0600 Subject: [PATCH 07/60] [Workplace Search] Add tests for remaining Sources components (#89026) * Remove history params We already replace the history.push functionality with KibanaLogic.values.navigateToUrl but the history object was still being passed around. * Add org sources container tests * Add tests for source router * Clean up leftover history imports * Add tests for SourcesRouter * Quick refactor for cleaner existence check Optional chaining FTW * Refactor to simplify setInterval logic This commit does a refactor to move the logic for polling for status to the logic file. In doing this I realized that we were intializing sources in the SourcesView, when we are actually already initializing sources in the components that use this, which are OrganizationSources and PrivateSources, the top-level containers. Because of this, I was able to remove the useEffect entireley, as the flash messages are cleared between page transitions in Kibana and the initialization of the sources ahppens in the containers. * Add tests for SourcesView * Fix type issue --- .../organization_sources.test.tsx | 63 +++++++++ .../content_sources/organization_sources.tsx | 3 +- .../views/content_sources/source_logic.ts | 4 +- .../content_sources/source_router.test.tsx | 120 ++++++++++++++++++ .../views/content_sources/source_router.tsx | 6 +- .../views/content_sources/sources_logic.ts | 28 +++- .../content_sources/sources_router.test.tsx | 60 +++++++++ .../content_sources/sources_view.test.tsx | 64 ++++++++++ .../views/content_sources/sources_view.tsx | 23 +--- 9 files changed, 337 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx new file mode 100644 index 0000000000000..1050150028aec --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import { shallow } from 'enzyme'; + +import React from 'react'; +import { Redirect } from 'react-router-dom'; + +import { contentSources } from '../../__mocks__/content_sources.mock'; + +import { Loading } from '../../../shared/loading'; +import { SourcesTable } from '../../components/shared/sources_table'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; + +import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; + +import { OrganizationSources } from './organization_sources'; + +describe('OrganizationSources', () => { + const initializeSources = jest.fn(); + const setSourceSearchability = jest.fn(); + + const mockValues = { + contentSources, + dataLoading: false, + }; + + beforeEach(() => { + setMockActions({ + initializeSources, + setSourceSearchability, + }); + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourcesTable)).toHaveLength(1); + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('returns redirect when no sources', () => { + setMockValues({ ...mockValues, contentSources: [] }); + const wrapper = shallow(); + + expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true)); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx index 880df3d086ccc..fdb536dd79771 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx @@ -27,10 +27,11 @@ const ORG_HEADER_DESCRIPTION = 'Organization sources are available to the entire organization and can be assigned to specific user groups.'; export const OrganizationSources: React.FC = () => { - const { initializeSources, setSourceSearchability } = useActions(SourcesLogic); + const { initializeSources, setSourceSearchability, resetSourcesState } = useActions(SourcesLogic); useEffect(() => { initializeSources(); + return resetSourcesState; }, []); const { dataLoading, contentSources } = useValues(SourcesLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index fe958db9d0232..2de70009c56a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -39,7 +39,7 @@ export interface SourceActions { ): { sourceId: string; source: { name: string } }; resetSourceState(): void; removeContentSource(sourceId: string): { sourceId: string }; - initializeSource(sourceId: string, history: object): { sourceId: string; history: object }; + initializeSource(sourceId: string): { sourceId: string }; getSourceConfigData(serviceType: string): { serviceType: string }; setButtonNotLoading(): void; } @@ -88,7 +88,7 @@ export const SourceLogic = kea>({ setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse, setContentFilterValue: (contentFilterValue: string) => contentFilterValue, setActivePage: (activePage: number) => activePage, - initializeSource: (sourceId: string, history: object) => ({ sourceId, history }), + initializeSource: (sourceId: string) => ({ sourceId }), initializeFederatedSummary: (sourceId: string) => ({ sourceId }), searchContentSourceDocuments: (sourceId: string) => ({ sourceId }), updateContentSource: (sourceId: string, source: { name: string }) => ({ sourceId, source }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx new file mode 100644 index 0000000000000..ac542f57b8fd4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useParams } from 'react-router-dom'; + +import { Route, Switch } from 'react-router-dom'; + +import { contentSources } from '../../__mocks__/content_sources.mock'; + +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { NAV } from '../../constants'; + +import { Loading } from '../../../shared/loading'; + +import { DisplaySettingsRouter } from './components/display_settings'; +import { Overview } from './components/overview'; +import { Schema } from './components/schema'; +import { SchemaChangeErrors } from './components/schema/schema_change_errors'; +import { SourceContent } from './components/source_content'; +import { SourceSettings } from './components/source_settings'; + +import { SourceRouter } from './source_router'; + +describe('SourceRouter', () => { + const initializeSource = jest.fn(); + const contentSource = contentSources[1]; + const customSource = contentSources[0]; + const mockValues = { + contentSource, + dataLoading: false, + }; + + beforeEach(() => { + setMockActions({ + initializeSource, + }); + setMockValues({ ...mockValues }); + (useParams as jest.Mock).mockImplementationOnce(() => ({ + sourceId: '1', + })); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders source routes (standard)', () => { + const wrapper = shallow(); + + expect(wrapper.find(Overview)).toHaveLength(1); + expect(wrapper.find(SourceSettings)).toHaveLength(1); + expect(wrapper.find(SourceContent)).toHaveLength(1); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(3); + }); + + it('renders source routes (custom)', () => { + setMockValues({ ...mockValues, contentSource: customSource }); + const wrapper = shallow(); + + expect(wrapper.find(DisplaySettingsRouter)).toHaveLength(1); + expect(wrapper.find(Schema)).toHaveLength(1); + expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(6); + }); + + it('handles breadcrumbs while loading (standard)', () => { + setMockValues({ + ...mockValues, + contentSource: {}, + }); + + const loadingBreadcrumbs = ['Sources', '...']; + + const wrapper = shallow(); + + const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0); + const contentBreadCrumb = wrapper.find(SetPageChrome).at(1); + const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2); + + expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.OVERVIEW]); + expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]); + expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]); + }); + + it('handles breadcrumbs while loading (custom)', () => { + setMockValues({ + ...mockValues, + contentSource: { serviceType: 'custom' }, + }); + + const loadingBreadcrumbs = ['Sources', '...']; + + const wrapper = shallow(); + + const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2); + const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3); + const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4); + + expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); + expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); + expect(displaySettingsBreadCrumb.prop('trail')).toEqual([ + ...loadingBreadcrumbs, + NAV.DISPLAY_SETTINGS, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index 089ef0cd46a00..f46743778a168 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -6,10 +6,9 @@ import React, { useEffect } from 'react'; -import { History } from 'history'; import { useActions, useValues } from 'kea'; import moment from 'moment'; -import { Route, Switch, useHistory, useParams } from 'react-router-dom'; +import { Route, Switch, useParams } from 'react-router-dom'; import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; @@ -46,14 +45,13 @@ import { SourceInfoCard } from './components/source_info_card'; import { SourceSettings } from './components/source_settings'; export const SourceRouter: React.FC = () => { - const history = useHistory() as History; const { sourceId } = useParams() as { sourceId: string }; const { initializeSource } = useActions(SourceLogic); const { contentSource, dataLoading } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); useEffect(() => { - initializeSource(sourceId, history); + initializeSource(sourceId); }, []); if (dataLoading) return ; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index ab71f76484561..0a3d047796f49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -77,6 +77,9 @@ interface ISourcesServerResponse { serviceTypes: Connector[]; } +let pollingInterval: number; +const POLLING_INTERVAL = 10000; + export const SourcesLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'sources_logic'], actions: { @@ -169,6 +172,7 @@ export const SourcesLogic = kea>( try { const response = await HttpLogic.values.http.get(route); + actions.pollForSourceStatusChanges(); actions.onInitializeSources(response); } catch (e) { flashAPIErrors(e); @@ -181,18 +185,20 @@ export const SourcesLogic = kea>( } }, // We poll the server and if the status update, we trigger a new fetch of the sources. - pollForSourceStatusChanges: async () => { + pollForSourceStatusChanges: () => { const { isOrganization } = AppLogic.values; if (!isOrganization) return; const serverStatuses = values.serverStatuses; - const sourceStatuses = await fetchSourceStatuses(isOrganization); + pollingInterval = window.setInterval(async () => { + const sourceStatuses = await fetchSourceStatuses(isOrganization); - sourceStatuses.some((source: ContentSourceStatus) => { - if (serverStatuses && serverStatuses[source.id] !== source.status.status) { - return actions.initializeSources(); - } - }); + sourceStatuses.some((source: ContentSourceStatus) => { + if (serverStatuses && serverStatuses[source.id] !== source.status.status) { + return actions.initializeSources(); + } + }); + }, POLLING_INTERVAL); }, setSourceSearchability: async ({ sourceId, searchable }) => { const { isOrganization } = AppLogic.values; @@ -235,6 +241,14 @@ export const SourcesLogic = kea>( resetFlashMessages: () => { clearFlashMessages(); }, + resetSourcesState: () => { + clearInterval(pollingInterval); + }, + }), + events: () => ({ + beforeUnmount() { + clearInterval(pollingInterval); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx new file mode 100644 index 0000000000000..7580203e759a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch, Redirect } from 'react-router-dom'; + +import { ADD_SOURCE_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; + +import { SourcesRouter } from './sources_router'; + +describe('SourcesRouter', () => { + const resetSourcesState = jest.fn(); + const mockValues = { + account: { canCreatePersonalSources: true }, + isOrganization: true, + hasPlatinumLicense: true, + }; + + beforeEach(() => { + setMockActions({ + resetSourcesState, + }); + setMockValues({ ...mockValues }); + }); + + it('renders sources routes', () => { + const TOTAL_ROUTES = 62; + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(TOTAL_ROUTES); + }); + + it('redirects when nonplatinum license and accountOnly context', () => { + setMockValues({ ...mockValues, hasPlatinumLicense: false }); + const wrapper = shallow(); + + expect(wrapper.find(Redirect).first().prop('from')).toEqual(ADD_SOURCE_PATH); + expect(wrapper.find(Redirect).first().prop('to')).toEqual(SOURCES_PATH); + }); + + it('redirects when cannot create sources', () => { + setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } }); + const wrapper = shallow(); + + expect(wrapper.find(Redirect).last().prop('from')).toEqual( + getSourcesPath(ADD_SOURCE_PATH, false) + ); + expect(wrapper.find(Redirect).last().prop('to')).toEqual(PERSONAL_SOURCES_PATH); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx new file mode 100644 index 0000000000000..7deb87f4311a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { EuiModal } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; + +import { SourcesView } from './sources_view'; + +describe('SourcesView', () => { + const resetPermissionsModal = jest.fn(); + const permissionsModal = { + addedSourceName: 'mySource', + serviceType: 'jira', + additionalConfiguration: true, + }; + + const mockValues = { + permissionsModal, + dataLoading: false, + }; + + const children =

test

; + + beforeEach(() => { + setMockActions({ + resetPermissionsModal, + }); + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow({children}); + + expect(wrapper.find('PermissionsModal')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow({children}); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('calls function on modal close', () => { + const wrapper = shallow({children}); + const modal = wrapper.find('PermissionsModal').dive().find(EuiModal); + modal.prop('onClose')(); + + expect(resetPermissionsModal).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index 7e3c14b203e9e..9e6c8f5b7319e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { useActions, useValues } from 'kea'; @@ -22,8 +22,6 @@ import { EuiText, } from '@elastic/eui'; -import { clearFlashMessages } from '../../../shared/flash_messages'; - import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; @@ -31,29 +29,14 @@ import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../ import { SourcesLogic } from './sources_logic'; -const POLLING_INTERVAL = 10000; - interface SourcesViewProps { children: React.ReactNode; } export const SourcesView: React.FC = ({ children }) => { - const { initializeSources, pollForSourceStatusChanges, resetPermissionsModal } = useActions( - SourcesLogic - ); - + const { resetPermissionsModal } = useActions(SourcesLogic); const { dataLoading, permissionsModal } = useValues(SourcesLogic); - useEffect(() => { - initializeSources(); - const pollingInterval = window.setInterval(pollForSourceStatusChanges, POLLING_INTERVAL); - - return () => { - clearFlashMessages(); - clearInterval(pollingInterval); - }; - }, []); - if (dataLoading) return ; const PermissionsModal = ({ @@ -113,7 +96,7 @@ export const SourcesView: React.FC = ({ children }) => { return ( <> - {!!permissionsModal && permissionsModal.additionalConfiguration && ( + {permissionsModal?.additionalConfiguration && ( Date: Thu, 21 Jan 2021 17:00:24 -0700 Subject: [PATCH 08/60] [Docs] Add geo threshold and containment docs (#88783) Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/alert-types.asciidoc | 2 +- docs/user/alerting/geo-alert-types.asciidoc | 127 ++++++++++++++++++ ...es-tracking-containment-action-options.png | Bin 0 -> 19963 bytes ...-types-tracking-containment-conditions.png | Bin 0 -> 22187 bytes .../images/alert-types-tracking-select.png | Bin 0 -> 37690 bytes ...rt-types-tracking-threshold-conditions.png | Bin 0 -> 37636 bytes docs/user/alerting/index.asciidoc | 1 + 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 docs/user/alerting/geo-alert-types.asciidoc create mode 100644 docs/user/alerting/images/alert-types-tracking-containment-action-options.png create mode 100644 docs/user/alerting/images/alert-types-tracking-containment-conditions.png create mode 100644 docs/user/alerting/images/alert-types-tracking-select.png create mode 100644 docs/user/alerting/images/alert-types-tracking-threshold-conditions.png diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 7de5ff56228cc..7c5a957d1cf79 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[alert-types]] -== Alert types +== Standard stack alert types {kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc new file mode 100644 index 0000000000000..c04cf4bca4320 --- /dev/null +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -0,0 +1,127 @@ +[role="xpack"] +[[geo-alert-types]] +== Geo alert types + +experimental[] Two additional stack alerts are available: +<> and <>. To enable, +add the following configuration to your `kibana.yml`: + +```yml +xpack.stack_alerts.enableGeoAlerting: true +``` + +As with other stack alerts, you need `all` access to the *Stack Alerts* feature +to be able to create and edit either of the geo alerts. +See <> for more information on configuring roles that provide access to this feature. + +[float] +=== Geo alert requirements + +To create either a *Tracking threshold* or a *Tracking containment* alert, the +following requirements must be present: + +- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, +and some form of entity identifier. An entity identifier is a `keyword` or `number` +field that consistently identifies the entity to be tracked. The data in this index should be dynamically +updating so that there are entity movements to alert upon. +- *Boundaries index or index pattern*: An index containing `geo_shape` data, such as boundary data and bounding box data. +This data is presumed to be static (not updating). Shape data matching the query is +harvested once when the alert is created and anytime after when the alert is re-enabled +after disablement. + +By design, current interval entity locations (_current_ is determined by `date` in +the *Tracked index or index pattern*) are queried to determine if they are contained +within any monitored boundaries. Entity +data should be somewhat "real time", meaning the dates of new documents aren’t older +than the current time minus the amount of the interval. If data older than +`now - ` is ingested, it won't trigger an alert. + +[float] +=== Creating a geo alert +Both *threshold* and *containment* alerts can be created by clicking the *Create* +button in the <>. +Complete the <>. +Select <> to generate an alert when an entity crosses a boundary, and you desire the +ability to highlight lines of crossing on a custom map. +Select +<> if an entity should send out constant alerts +while contained within a boundary (this feature is optional) or if the alert is generally +just more focused around activity when an entity exists within a shape. + +[role="screenshot"] +image::images/alert-types-tracking-select.png[Choosing a tracking alert type] + +[NOTE] +================================================== +With recent advances in the alerting framework, most of the features +available in Tracking threshold alerts can be replicated with just +a little more work in Tracking containment alerts. The capabilities of Tracking +threshold alerts may be deprecated or folded into Tracking containment alerts +in the future. +================================================== + +[float] +[[alert-type-tracking-threshold]] +=== Tracking threshold +The Tracking threshold alert type runs an {es} query over indices, comparing the latest +entity locations with their previous locations. In the event that an entity has crossed a +boundary from the selected boundary index, an alert may be generated. + +[float] +==== Defining the conditions +Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the +condition to detect, as well as 2 Kuery bars used to provide additional filtering +context for each of the indices. + +[role="screenshot"] +image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] + + +Delayed evaluation offset:: If a data source lags or is intermittent, you may supply +an optional value to evaluate alert conditions following a fixed delay. For instance, if data +is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation +offset* of `10 minutes` would ensure that alertable instances are still captured. +Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +By:: This clause specifies the field to use in the previously provided +*index or index pattern* for tracking Entities. An entity is a `keyword` +or `number` field that consistently identifies the entity to be tracked. +When entity:: This clause specifies which crossing option to track. The values +*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions +should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit +from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances +or exits. +Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +identifying boundaries, and an optional *Human-readable boundary name* for better alerting +messages. + +[float] +[[alert-type-tracking-containment]] +=== Tracking containment +The Tracking containment alert type runs an {es} query over indices, determining if any +documents are currently contained within any boundaries from the specified boundary index. +In the event that an entity is contained within a boundary, an alert may be generated. + +[float] +==== Defining the conditions +Tracking containment alerts have 3 clauses that define the condition to detect, +as well as 2 Kuery bars used to provide additional filtering context for each of the indices. + +[role="screenshot"] +image::images/alert-types-tracking-containment-conditions.png[Five clauses define the condition to detect] + +Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +When entity:: This clause specifies which crossing option to track. The values +*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions +should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit +from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances +or exits. +Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +identifying boundaries, and an optional *Human-readable boundary name* for better alerting +messages. + +Conditions for how an alert is tracked can be specified uniquely for each individual action. +An alert can be triggered either when a containment condition is met or when an entity +is no longer contained. + +[role="screenshot"] +image::images/alert-types-tracking-containment-action-options.png[Five clauses define the condition to detect] diff --git a/docs/user/alerting/images/alert-types-tracking-containment-action-options.png b/docs/user/alerting/images/alert-types-tracking-containment-action-options.png new file mode 100644 index 0000000000000000000000000000000000000000..c0a045f8273829d7a9fbb0d07b19c2a137dbc46f GIT binary patch literal 19963 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 JPDHLkV1n`+-|qkb literal 0 HcmV?d00001 diff --git a/docs/user/alerting/images/alert-types-tracking-containment-conditions.png b/docs/user/alerting/images/alert-types-tracking-containment-conditions.png new file mode 100644 index 0000000000000000000000000000000000000000..32c17d2245d23c9d526e252b802deb75c6984931 GIT binary patch literal 22187 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

Z=b$?CE%Gp~U{yd9HNKQ^@G22Il7oCaCoLa&z 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 Date: Thu, 21 Jan 2021 18:27:27 -0700 Subject: [PATCH 09/60] [Maps] fix setting "apply global time" switch not working with blended vector layer (#88996) * [Maps] fix setting "apply global time" switch not working with blended vector layer * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../blended_vector_layer.test.tsx | 46 +++++++++++-------- .../blended_vector_layer.ts | 1 + .../maps/public/classes/layers/layer.tsx | 5 +- .../connected_components/mb_map/mb_map.tsx | 2 +- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx index 1321593f015c0..e029480bd8616 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx @@ -7,7 +7,10 @@ import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; import { BlendedVectorLayer } from './blended_vector_layer'; import { ESSearchSource } from '../../sources/es_search_source'; -import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; +import { + AbstractESSourceDescriptor, + ESGeoGridSourceDescriptor, +} from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services', () => { return { @@ -53,27 +56,12 @@ describe('getSource', () => { expect(source.cloneDescriptor().type).toBe(SOURCE_TYPES.ES_GEO_GRID); }); - test('cluster source applyGlobalQuery should be true when document source applyGlobalQuery is true', async () => { - const blendedVectorLayer = new BlendedVectorLayer({ - source: new ESSearchSource(documentSourceDescriptor), - layerDescriptor: BlendedVectorLayer.createDescriptor( - { - sourceDescriptor: documentSourceDescriptor, - __dataRequests: [clusteredDataRequest], - }, - mapColors - ), - }); - - const source = blendedVectorLayer.getSource(); - expect((source.cloneDescriptor() as ESGeoGridSourceDescriptor).applyGlobalQuery).toBe(true); - }); - - test('cluster source applyGlobalQuery should be false when document source applyGlobalQuery is false', async () => { + test('cluster source AbstractESSourceDescriptor properties should mirror document source AbstractESSourceDescriptor properties', async () => { const blendedVectorLayer = new BlendedVectorLayer({ source: new ESSearchSource({ ...documentSourceDescriptor, applyGlobalQuery: false, + applyGlobalTime: false, }), layerDescriptor: BlendedVectorLayer.createDescriptor( { @@ -85,7 +73,27 @@ describe('getSource', () => { }); const source = blendedVectorLayer.getSource(); - expect((source.cloneDescriptor() as ESGeoGridSourceDescriptor).applyGlobalQuery).toBe(false); + const sourceDescriptor = source.cloneDescriptor() as ESGeoGridSourceDescriptor; + const abstractEsSourceDescriptor: AbstractESSourceDescriptor = { + // Purposely grabbing properties instead of using spread operator + // to ensure type check will fail when new properties are added to AbstractESSourceDescriptor. + // In the event of type check failure, ensure test is updated with new property and that new property + // is correctly passed to clustered source descriptor. + type: sourceDescriptor.type, + id: sourceDescriptor.id, + indexPatternId: sourceDescriptor.indexPatternId, + geoField: sourceDescriptor.geoField, + applyGlobalQuery: sourceDescriptor.applyGlobalQuery, + applyGlobalTime: sourceDescriptor.applyGlobalTime, + }; + expect(abstractEsSourceDescriptor).toEqual({ + type: sourceDescriptor.type, + id: sourceDescriptor.id, + geoField: 'myGeoField', + indexPatternId: 'myIndexPattern', + applyGlobalQuery: false, + applyGlobalTime: false, + } as AbstractESSourceDescriptor); }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 825f6ed74777a..5b33738a91a28 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -62,6 +62,7 @@ function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle requestType: RENDER_AS.POINT, }); clusterSourceDescriptor.applyGlobalQuery = documentSource.getApplyGlobalQuery(); + clusterSourceDescriptor.applyGlobalTime = documentSource.getApplyGlobalTime(); clusterSourceDescriptor.metrics = [ { type: AGG_TYPE.COUNT, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 060ff4d46fa2a..fe13e4f0ac2f6 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -5,6 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Map as MbMap } from 'mapbox-gl'; import { Query } from 'src/plugins/data/public'; import _ from 'lodash'; import React, { ReactElement } from 'react'; @@ -68,7 +69,7 @@ export interface ILayer { ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; canShowTooltip(): boolean; - syncLayerWithMB(mbMap: unknown): void; + syncLayerWithMB(mbMap: MbMap): void; getLayerTypeIconName(): string; isDataLoaded(): boolean; getIndexPatternIds(): string[]; @@ -418,7 +419,7 @@ export class AbstractLayer implements ILayer { return false; } - syncLayerWithMB(mbMap: unknown) { + syncLayerWithMB(mbMap: MbMap) { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 4dc765f1704a0..820453f166a46 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -332,7 +332,7 @@ export class MBMap extends Component { this.props.layerList, this.props.spatialFiltersLayer ); - this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap)); + this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!)); syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList); }; From a0bfdf87fd51cad4973a6d07aca66a90c98b2ed3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 21 Jan 2021 19:35:40 -0700 Subject: [PATCH 10/60] [Maps] fix Filter shape stops showing feedback when data refreshes (#89009) * [Maps] fix Filter shape stops showing feedback when data refreshes * update comment * add curly braces around if --- .../mb_map/sort_layers.test.ts | 2 ++ .../connected_components/mb_map/sort_layers.ts | 16 ++++++++++++++++ .../public/connected_components/mb_map/utils.js | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts index 9e85c7b04b266..4e9cb499cf704 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts @@ -135,6 +135,7 @@ describe('sortLayer', () => { { id: `${BRAVO_LAYER_ID}_circle`, type: 'circle' } as MbLayer, { id: `${SPATIAL_FILTERS_LAYER_ID}_fill`, type: 'fill' } as MbLayer, { id: `${SPATIAL_FILTERS_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { id: `gl-draw-polygon-fill-active.cold`, type: 'fill' } as MbLayer, { id: `${CHARLIE_LAYER_ID}_text`, type: 'symbol', @@ -158,6 +159,7 @@ describe('sortLayer', () => { 'alpha_text', 'alpha_circle', 'charlie_text', + 'gl-draw-polygon-fill-active.cold', 'SPATIAL_FILTERS_LAYER_ID_fill', 'SPATIAL_FILTERS_LAYER_ID_circle', ]); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts index dda43269e32d8..adf68ffb310bc 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts @@ -28,6 +28,10 @@ export function getIsTextLayer(mbLayer: MbLayer) { }); } +export function isGlDrawLayer(mbLayerId: string) { + return mbLayerId.startsWith('gl-draw'); +} + function doesMbLayerBelongToMapLayerAndClass( mapLayer: ILayer, mbLayer: MbLayer, @@ -118,6 +122,18 @@ export function syncLayerOrder(mbMap: MbMap, spatialFiltersLayer: ILayer, layerL } let beneathMbLayerId = getBottomMbLayerId(mbLayers, spatialFiltersLayer, LAYER_CLASS.ANY); + // Ensure gl-draw layers are on top of all layerList layers + const glDrawLayer = ({ + ownsMbLayerId: (mbLayerId: string) => { + return isGlDrawLayer(mbLayerId); + }, + } as unknown) as ILayer; + moveMapLayer(mbMap, mbLayers, glDrawLayer, LAYER_CLASS.ANY, beneathMbLayerId); + const glDrawBottomMbLayerId = getBottomMbLayerId(mbLayers, glDrawLayer, LAYER_CLASS.ANY); + if (glDrawBottomMbLayerId) { + beneathMbLayerId = glDrawBottomMbLayerId; + } + // Sort map layer labels [...layerList] .reverse() diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js index f12f34061756f..2f8852174c29e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js @@ -5,6 +5,7 @@ */ import { RGBAImage } from './image_utils'; +import { isGlDrawLayer } from './sort_layers'; export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) { const mbStyle = mbMap.getStyle(); @@ -17,7 +18,7 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLa } // ignore gl-draw layers - if (mbLayer.id.startsWith('gl-draw')) { + if (isGlDrawLayer(mbLayer.id)) { return; } From 58c54b6f8583eea111f5a039c422bcb17de681f6 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 22 Jan 2021 09:02:42 -0500 Subject: [PATCH 11/60] Add a high level overview of the Kibana platform and plugin development. (#87560) * add platform intro * address code review comments (wip) * incorporate more information about plugins * put back Josh's suggestion * Update dev_docs/kibana_platform_plugin_intro.mdx Co-authored-by: Brandon Kobel * try another angle * further refinements * sp * Update kibana_platform_plugin_intro.mdx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Brandon Kobel --- .../kibana_platform_plugin_end_user.png | Bin 0 -> 355010 bytes dev_docs/assets/platform_plugin_cycle.png | Bin 0 -> 202395 bytes dev_docs/kibana_platform_plugin_intro.mdx | 305 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 dev_docs/assets/kibana_platform_plugin_end_user.png create mode 100644 dev_docs/assets/platform_plugin_cycle.png create mode 100644 dev_docs/kibana_platform_plugin_intro.mdx diff --git a/dev_docs/assets/kibana_platform_plugin_end_user.png b/dev_docs/assets/kibana_platform_plugin_end_user.png new file mode 100644 index 0000000000000000000000000000000000000000..a0e32a35ffe6054cc1c17341eb8052d3c901b92b GIT binary patch literal 355010 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%0DqU|vl=DQohkO&o$aw2QW3|*pSvUX!faEH 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 literal 0 HcmV?d00001 diff --git a/dev_docs/assets/platform_plugin_cycle.png b/dev_docs/assets/platform_plugin_cycle.png new file mode 100644 index 0000000000000000000000000000000000000000..533a38c4ed9c4db71b94c00a86c2aad026fd9aa6 GIT binary patch literal 202395 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%0D1lN=X?k?Bw1Z9_x30+ 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%0A$c=m8eAa1%T_TEM{JGJxKo06rZ4rdGQer@$1 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±a2W&NG>}5gMVrm`i{MFil6ENK1y~tibDBSWy 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 provides basic utilities to search, query, and filter data in Elasticsearch. +This code is not part of Core, but is still fundamental for building a plugin, + and we strongly encourage using this service over querying Elasticsearch directly. + + +We currently have three kinds of public services: + + - platform services provided by `core` + - platform services provided by plugins, that can, and should, be used by every plugin (e.g. ) . + - shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). + +Two common questions we encounter are: + +1. Which services are platform services? +2. What is the difference between platform code supplied by core, and platform code supplied by plugins? + +We don't have great answers to those questions today. Currently, the best answers we have are: + +1. Platform plugins are _usually_ plugins that are managed by the Platform Group, but we are starting to see some exceptions. +2. `core` code contains the most fundamental and stable services needed for plugin development. Everything else goes in a plugin. + +We will continue to focus on adding clarity around these types of services and what developers can expect from each. + + + + + +When the Kibana platform and plugin infrastructure was built, we thought of two types of code: core services, and other plugin services. We planned to keep the most stable and fundamental +code needed to build plugins inside core. + +In reality, we ended up with many platform-like services living outside of core, with no (short term) intention of moving them. We highly encourage plugin developers to use +them, so we consider them part of platform services. + +When we built our platform system, we also thought we'd end up with only a handful of large plugins outside core. Users could turn certain plugins off, to minimize the code + footprint and speed up Kibana. + +In reality, our plugin model ended up being used like micro-services. Plugins are the only form of encapsulation we provide developers, and they liked it! However, we ended + up with a ton of small plugins, that developers never intended to be uninstallable, nor tested in this manner. We are considering ways to provide developers the ability to build services + with the encapsulation + they desire, without the need to build a plugin. + +Another side effect of having many small plugins is that common code often ends up extracted into another plugin. Use case specific utilities are exported, + that are not meant to be used in a general manner. This makes our definition of "platform code" a bit trickier to define. We'd like to say "The platform is made up of + every publically exposed service", but in today's world, that wouldn't be a very accurate picture. + +We recognize the need to better clarify the relationship between core functionality, platform-like plugin functionality, and functionality exposed by other plugins. + It's something we will be working on! + + + +The main difference between core functionality and functionality supplied by plugins, is in how it is accessed. Core is +passed to plugins as the first parameter to their `start` and `setup` lifecycle functions, while plugin supplied functionality is passed as the +second parameter. Plugin dependencies must be declared explicitly inside the `kibana.json` file. Core functionality is always provided. Read the +section on [how plugins interact with eachother and core](#how-plugins-interact-with-each-other-and-core) for more information. + +## The anatomy of a plugin + +Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, +or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, +and you interact with Core and other plugins in the same way. + +The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: + +``` +plugins/ + demo + kibana.json [1] + public + index.ts [2] + plugin.ts [3] + server + index.ts [4] + plugin.ts [5] +``` + +### [1] kibana.json + +`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: + +``` +{ + "id": "demo", + "version": "kibana", + "server": true, + "ui": true +} +``` + +### [2] public/index.ts + +`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of + core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. + +``` +import type { PluginInitializerContext } from 'kibana/server'; +import { DemoPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new DemoPlugin(initializerContext); +} +``` + +### [3] public/plugin.ts + +`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry + point, but all plugins at Elastic should be consistent in this way. + + + ```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} + ``` + + +### [4] server/index.ts + +`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: + +### [5] server/plugin.ts + +`server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part: + +```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} +``` + +Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain +considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. + +## Plugin lifecycles & Core services + +The various independent domains that make up core are represented by a series of services. Those services expose public interfaces that are provided to all plugins. +Services expose different features at different parts of their lifecycle. We describe the lifecycle of core services and plugins with specifically-named functions on the service definition. + +Kibana has three lifecycles: setup, start, and stop. Each plugin’s setup function is called sequentially while Kibana is setting up on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. The stop functions are called sequentially while Kibana is gracefully shutting down the server or when the browser tab or window is being closed. + +The table below explains how each lifecycle relates to the state of Kibana. + +| lifecycle | purpose | server | browser | +| ---------- | ------ | ------- | ----- | +| setup | perform "registration" work to setup environment for runtime |configure REST API endpoint, register saved object types, etc. | configure application routes in SPA, register custom UI elements in extension points, etc. | +| start | bootstrap runtime logic | respond to an incoming request, request Elasticsearch server, etc. | start polling Kibana server, update DOM tree in response to user interactions, etc.| +| stop | cleanup runtime | dispose of active handles before the server shutdown. | store session data in the LocalStorage when the user navigates away from Kibana, etc. | + +Different service interfaces can and will be passed to setup, start, and stop because certain functionality makes sense in the context of a running plugin while other types +of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. + +## How plugin's interact with each other, and Core + +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. +For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, +a plugin just accesses it off of the first argument: + +```ts +import type { CoreSetup } from 'kibana/server'; + +export class DemoPlugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + // handler is called when '/path' resource is requested with `GET` method + router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + } +} +``` + +Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. +Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a + dependency in it’s kibana.json manifest file. + +** foobar plugin.ts: ** + +``` +import type { Plugin } from 'kibana/server'; +export interface FoobarPluginSetup { [1] + getFoo(): string; +} + +export interface FoobarPluginStart { [1] + getBar(): string; +} + +export class MyPlugin implements Plugin { + public setup(): FoobarPluginSetup { + return { + getFoo() { + return 'foo'; + }, + }; + } + + public start(): FoobarPluginStart { + return { + getBar() { + return 'bar'; + }, + }; + } +} +``` +[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. + + +** demo kibana.json** + +``` +{ + "id": "demo", + "requiredPlugins": ["foobar"], + "server": true, + "ui": true +} +``` + +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of setup and/or start: + +```ts +import type { CoreSetup, CoreStart } from 'kibana/server'; +import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; + +interface DemoSetupPlugins { [1] + foobar: FoobarPluginSetup; +} + +interface DemoStartPlugins { + foobar: FoobarPluginStart; +} + +export class DemoPlugin { + public setup(core: CoreSetup, plugins: DemoSetupPlugins) { [2] + const { foobar } = plugins; + foobar.getFoo(); // 'foo' + foobar.getBar(); // throws because getBar does not exist + } + + public start(core: CoreStart, plugins: DemoStartPlugins) { [3] + const { foobar } = plugins; + foobar.getFoo(); // throws because getFoo does not exist + foobar.getBar(); // 'bar' + } + + public stop() {} +} +``` + +[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. + +[2] These manually constructed types should then be used to specify the type of the second argument to the plugin. + +[3] Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle. From 49d95f6fb1678af9db7243bd5b5026d9dad47adb Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Fri, 22 Jan 2021 12:12:59 -0500 Subject: [PATCH 12/60] [Fleet] Add `updateFleetRoleIfExists()` in order to update `fleet_enroll` permissions if role already exists (#88000) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/server/services/setup.ts | 41 ++++-- .../fleet_api_integration/apis/fleet_setup.ts | 124 ++++++++++++++++++ .../test/fleet_api_integration/apis/index.js | 2 + 3 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/fleet_setup.ts diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 1ce7b1d85c8e4..0dcdfeb7b3801 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -59,6 +59,7 @@ async function createSetupSideEffects( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), + updateFleetRoleIfExists(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); @@ -126,15 +127,25 @@ async function createSetupSideEffects( return { isIntialized: true }; } -export async function setupFleet( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - callCluster: CallESAsCurrentUser, - options?: { forceRecreate?: boolean } -) { - // Create fleet_enroll role - // This should be done directly in ES at some point - const res = await callCluster('transport.request', { +async function updateFleetRoleIfExists(callCluster: CallESAsCurrentUser) { + try { + await callCluster('transport.request', { + method: 'GET', + path: `/_security/role/${FLEET_ENROLL_ROLE}`, + }); + } catch (e) { + if (e.status === 404) { + return; + } + + throw e; + } + + return putFleetRole(callCluster); +} + +async function putFleetRole(callCluster: CallESAsCurrentUser) { + return callCluster('transport.request', { method: 'PUT', path: `/_security/role/${FLEET_ENROLL_ROLE}`, body: { @@ -156,6 +167,18 @@ export async function setupFleet( ], }, }); +} + +export async function setupFleet( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + callCluster: CallESAsCurrentUser, + options?: { forceRecreate?: boolean } +) { + // Create fleet_enroll role + // This should be done directly in ES at some point + const res = await putFleetRole(callCluster); + // If the role is already created skip the rest unless you have forceRecreate set to true if (options?.forceRecreate !== true && res.role.created === false) { return; diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts new file mode 100644 index 0000000000000..8e9a01b28ea9b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + + describe('fleet_setup', () => { + skipIfNoDockerRegistry(providerContext); + beforeEach(async () => { + try { + await es.security.deleteUser({ + username: 'fleet_enroll', + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + try { + await es.security.deleteRole({ + name: 'fleet_enroll', + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + }); + + it('should not create a fleet_enroll role if one does not already exist', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/setup`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(apiResponse.isInitialized).to.be(true); + + try { + await es.security.getUser({ + username: 'fleet_enroll', + }); + } catch (e) { + expect(e.meta?.statusCode).to.eql(404); + } + }); + + it('should update the fleet_enroll role with new index permissions if one does already exist', async () => { + try { + await es.security.putRole({ + name: 'fleet_enroll', + body: { + cluster: ['monitor', 'manage_api_key'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + ], + privileges: ['write', 'create_index', 'indices:admin/auto_create'], + allow_restricted_indices: false, + }, + ], + applications: [], + run_as: [], + metadata: {}, + transient_metadata: { enabled: true }, + }, + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + + const { body: apiResponse } = await supertest + .post(`/api/fleet/setup`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(apiResponse.isInitialized).to.be(true); + + const { body: roleResponse } = await es.security.getRole({ + name: 'fleet_enroll', + }); + expect(roleResponse).to.have.key('fleet_enroll'); + expect(roleResponse.fleet_enroll).to.eql({ + cluster: ['monitor', 'manage_api_key'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + '.logs-endpoint.diagnostic.collection-*', + '.ds-.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['write', 'create_index', 'indices:admin/auto_create'], + allow_restricted_indices: false, + }, + ], + applications: [], + run_as: [], + metadata: {}, + transient_metadata: { enabled: true }, + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 0d634f60e282f..f472599652224 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -7,6 +7,8 @@ export default function ({ loadTestFile }) { describe('Fleet Endpoints', function () { this.tags('ciGroup10'); + // Fleet setup + loadTestFile(require.resolve('./fleet_setup')); // Agent setup loadTestFile(require.resolve('./agents_setup')); // Agents From d81ab83c1683072e4df812db4056dd2fdb382016 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Fri, 22 Jan 2021 19:52:47 +0100 Subject: [PATCH 13/60] [examples] expressions explorer (#88344) --- ...-public.expressionsinspectoradapter.ast.md | 11 ++ ...blic.expressionsinspectoradapter.logast.md | 22 ++++ ...ions-public.expressionsinspectoradapter.md | 24 ++++ ...ibana-plugin-plugins-expressions-public.md | 1 + examples/expressions_explorer/README.md | 8 ++ examples/expressions_explorer/kibana.json | 10 ++ .../public/actions/navigate_action.ts | 21 ++++ .../public/actions/navigate_trigger.ts | 15 +++ .../public/actions_and_expressions.tsx | 102 +++++++++++++++ examples/expressions_explorer/public/app.tsx | 78 ++++++++++++ .../public/editor/expression_editor.tsx | 35 ++++++ .../public/functions/button.ts | 50 ++++++++ examples/expressions_explorer/public/index.ts | 11 ++ .../public/inspector/ast_debug_view.tsx | 78 ++++++++++++ .../inspector/expressions_inspector_view.tsx | 98 +++++++++++++++ .../expressions_inspector_view_wrapper.tsx | 17 +++ .../public/inspector/index.ts | 25 ++++ .../expressions_explorer/public/plugin.tsx | 87 +++++++++++++ .../public/render_expressions.tsx | 99 +++++++++++++++ .../public/renderers/button.tsx | 40 ++++++ .../public/run_expressions.tsx | 118 ++++++++++++++++++ examples/expressions_explorer/tsconfig.json | 18 +++ .../expressions/common/execution/execution.ts | 5 + .../util/expressions_inspector_adapter.ts | 22 ++++ src/plugins/expressions/common/util/index.ts | 1 + src/plugins/expressions/public/index.ts | 1 + src/plugins/expressions/public/public.api.md | 10 ++ test/examples/config.js | 1 + .../expressions_explorer/expressions.ts | 44 +++++++ test/examples/expressions_explorer/index.ts | 28 +++++ .../canvas_plugin_src/renderers/debug.tsx | 8 +- 31 files changed, 1085 insertions(+), 3 deletions(-) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md create mode 100644 examples/expressions_explorer/README.md create mode 100644 examples/expressions_explorer/kibana.json create mode 100644 examples/expressions_explorer/public/actions/navigate_action.ts create mode 100644 examples/expressions_explorer/public/actions/navigate_trigger.ts create mode 100644 examples/expressions_explorer/public/actions_and_expressions.tsx create mode 100644 examples/expressions_explorer/public/app.tsx create mode 100644 examples/expressions_explorer/public/editor/expression_editor.tsx create mode 100644 examples/expressions_explorer/public/functions/button.ts create mode 100644 examples/expressions_explorer/public/index.ts create mode 100644 examples/expressions_explorer/public/inspector/ast_debug_view.tsx create mode 100644 examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx create mode 100644 examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx create mode 100644 examples/expressions_explorer/public/inspector/index.ts create mode 100644 examples/expressions_explorer/public/plugin.tsx create mode 100644 examples/expressions_explorer/public/render_expressions.tsx create mode 100644 examples/expressions_explorer/public/renderers/button.tsx create mode 100644 examples/expressions_explorer/public/run_expressions.tsx create mode 100644 examples/expressions_explorer/tsconfig.json create mode 100644 src/plugins/expressions/common/util/expressions_inspector_adapter.ts create mode 100644 test/examples/expressions_explorer/expressions.ts create mode 100644 test/examples/expressions_explorer/index.ts diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md new file mode 100644 index 0000000000000..0fdf36bc719ec --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) + +## ExpressionsInspectorAdapter.ast property + +Signature: + +```typescript +get ast(): any; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md new file mode 100644 index 0000000000000..671270a5c78ce --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [logAST](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) + +## ExpressionsInspectorAdapter.logAST() method + +Signature: + +```typescript +logAST(ast: any): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | any | | + +Returns: + +`void` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md new file mode 100644 index 0000000000000..23d542a0f69eb --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) + +## ExpressionsInspectorAdapter class + +Signature: + +```typescript +export declare class ExpressionsInspectorAdapter extends EventEmitter +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) | | any | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [logAST(ast)](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) | | | + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index 1b97c9e11f83c..e3eb7a34175ee 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -16,6 +16,7 @@ | [ExpressionRenderer](./kibana-plugin-plugins-expressions-public.expressionrenderer.md) | | | [ExpressionRendererRegistry](./kibana-plugin-plugins-expressions-public.expressionrendererregistry.md) | | | [ExpressionRenderHandler](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.md) | | +| [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) | | | [ExpressionsPublicPlugin](./kibana-plugin-plugins-expressions-public.expressionspublicplugin.md) | | | [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | ExpressionsService class is used for multiple purposes:1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. ExpressionsService defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. ExpressionsService creates a bound version of all exported contract functions. 5. Functions are bound the way there are:\`\`\`ts registerFunction = (...args: Parameters<Executor\['registerFunction'\]> ): ReturnType<Executor\['registerFunction'\]> => this.executor.registerFunction(...args); \`\`\`so that JSDoc appears in developers IDE when they use those plugins.expressions.registerFunction(. | | [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | | diff --git a/examples/expressions_explorer/README.md b/examples/expressions_explorer/README.md new file mode 100644 index 0000000000000..ead0ca758f8e5 --- /dev/null +++ b/examples/expressions_explorer/README.md @@ -0,0 +1,8 @@ +## expressions explorer + +This example expressions explorer app shows how to: + - to run expression + - to render expression output + - emit events from expression renderer and handle them + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json new file mode 100644 index 0000000000000..038b7eea0ef21 --- /dev/null +++ b/examples/expressions_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "expressionsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/examples/expressions_explorer/public/actions/navigate_action.ts b/examples/expressions_explorer/public/actions/navigate_action.ts new file mode 100644 index 0000000000000..d29a9e6b345b6 --- /dev/null +++ b/examples/expressions_explorer/public/actions/navigate_action.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { createAction } from '../../../../src/plugins/ui_actions/public'; + +export const ACTION_NAVIGATE = 'ACTION_NAVIGATE'; + +export const createNavigateAction = () => + createAction({ + id: ACTION_NAVIGATE, + type: ACTION_NAVIGATE, + getDisplayName: () => 'Navigate', + execute: async (event: any) => { + window.location.href = event.href; + }, + }); diff --git a/examples/expressions_explorer/public/actions/navigate_trigger.ts b/examples/expressions_explorer/public/actions/navigate_trigger.ts new file mode 100644 index 0000000000000..eacbd968eaa93 --- /dev/null +++ b/examples/expressions_explorer/public/actions/navigate_trigger.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Trigger } from '../../../../src/plugins/ui_actions/public'; + +export const NAVIGATE_TRIGGER_ID = 'NAVIGATE_TRIGGER_ID'; + +export const navigateTrigger: Trigger = { + id: NAVIGATE_TRIGGER_ID, +}; diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx new file mode 100644 index 0000000000000..6e2eebcde4a0f --- /dev/null +++ b/examples/expressions_explorer/public/actions_and_expressions.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger'; + +interface Props { + expressions: ExpressionsStart; + actions: UiActionsStart; +} + +export function ActionsExpressionsExample({ expressions, actions }: Props) { + const [expression, updateExpression] = useState( + 'button name="click me" href="http://www.google.com"' + ); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + const handleEvents = (event: any) => { + if (event.id !== 'NAVIGATE') return; + // enrich event context with some extra data + event.baseUrl = 'http://www.google.com'; + + actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value); + }; + + return ( + + + + +

Actions from expression renderers

+ + + + + + + + + Here you can play with sample `button` which takes a url as configuration and + displays a button which emits custom BUTTON_CLICK trigger to which we have attached + a custom action which performs the navigation. + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+ + ); +} diff --git a/examples/expressions_explorer/public/app.tsx b/examples/expressions_explorer/public/app.tsx new file mode 100644 index 0000000000000..d72cf08128a5a --- /dev/null +++ b/examples/expressions_explorer/public/app.tsx @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + EuiText, + EuiLink, +} from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; +import { RunExpressionsExample } from './run_expressions'; +import { RenderExpressionsExample } from './render_expressions'; +import { ActionsExpressionsExample } from './actions_and_expressions'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; + actions: UiActionsStart; +} + +const ExpressionsExplorer = ({ expressions, inspector, actions }: Props) => { + return ( + + + Expressions Explorer + + + +

+ There are a couple of ways to run the expressions. Below some of the options are + demonstrated. You can read more about it{' '} + + here + +

+
+ + + + + + + + + + + + +
+
+
+
+ ); +}; + +export const renderApp = (props: Props, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/expressions_explorer/public/editor/expression_editor.tsx b/examples/expressions_explorer/public/editor/expression_editor.tsx new file mode 100644 index 0000000000000..e3dbb5998b92e --- /dev/null +++ b/examples/expressions_explorer/public/editor/expression_editor.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +export function ExpressionEditor({ value, onChange }: Props) { + return ( + {}} + aria-label="Code Editor" + /> + ); +} diff --git a/examples/expressions_explorer/public/functions/button.ts b/examples/expressions_explorer/public/functions/button.ts new file mode 100644 index 0000000000000..8c39aa2743b30 --- /dev/null +++ b/examples/expressions_explorer/public/functions/button.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/common'; + +interface Arguments { + href: string; + name: string; +} + +export type ExpressionFunctionButton = ExpressionFunctionDefinition< + 'button', + unknown, + Arguments, + unknown +>; + +export const buttonFn: ExpressionFunctionButton = { + name: 'button', + args: { + href: { + help: i18n.translate('expressions.functions.font.args.href', { + defaultMessage: 'Link to which to navigate', + }), + types: ['string'], + required: true, + }, + name: { + help: i18n.translate('expressions.functions.font.args.name', { + defaultMessage: 'Name of the button', + }), + types: ['string'], + default: 'button', + }, + }, + help: 'Configures the button', + fn: (input: unknown, args: Arguments) => { + return { + type: 'render', + as: 'button', + value: args, + }; + }, +}; diff --git a/examples/expressions_explorer/public/index.ts b/examples/expressions_explorer/public/index.ts new file mode 100644 index 0000000000000..a6dbbc9198f44 --- /dev/null +++ b/examples/expressions_explorer/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ExpressionsExplorerPlugin } from './plugin'; + +export const plugin = () => new ExpressionsExplorerPlugin(); diff --git a/examples/expressions_explorer/public/inspector/ast_debug_view.tsx b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx new file mode 100644 index 0000000000000..d860ff30bd8e9 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiTreeView, EuiDescriptionList, EuiCodeBlock, EuiText, EuiSpacer } from '@elastic/eui'; + +interface Props { + ast: any; +} + +const decorateAst = (ast: any, nodeClicked: any) => { + return ast.chain.map((link: any) => { + return { + id: link.function + Math.random(), + label: link.function, + callback: () => { + nodeClicked(link.debug); + }, + children: Object.keys(link.arguments).reduce((result: any, key: string) => { + if (typeof link.arguments[key] === 'object') { + // result[key] = decorateAst(link.arguments[key]); + } + return result; + }, []), + }; + }); +}; + +const prepareNode = (key: string, value: any) => { + if (key === 'args') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'output' || key === 'input') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'success') { + return value ? 'true' : 'false'; + } else return {value}; +}; + +export function AstDebugView({ ast }: Props) { + const [nodeInfo, setNodeInfo] = useState([] as any[]); + const items = decorateAst(ast, (node: any) => { + setNodeInfo( + Object.keys(node).map((key) => ({ + title: key, + description: prepareNode(key, node[key]), + })) + ); + }); + + return ( +
+ List of executed expression functions: + + + Details of selected function: + +
+ ); +} diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx new file mode 100644 index 0000000000000..1233735072d04 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { InspectorViewProps, Adapters } from '../../../../src/plugins/inspector/public'; +import { AstDebugView } from './ast_debug_view'; + +interface ExpressionsInspectorViewComponentState { + ast: any; + adapters: Adapters; +} + +class ExpressionsInspectorViewComponent extends Component< + InspectorViewProps, + ExpressionsInspectorViewComponentState +> { + static propTypes = { + adapters: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + }; + + state = {} as ExpressionsInspectorViewComponentState; + + static getDerivedStateFromProps( + nextProps: Readonly, + state: ExpressionsInspectorViewComponentState + ) { + if (state && nextProps.adapters === state.adapters) { + return null; + } + + const { ast } = nextProps.adapters.expression; + + return { + adapters: nextProps.adapters, + ast, + }; + } + + onUpdateData = (ast: any) => { + this.setState({ + ast, + }); + }; + + componentDidMount() { + this.props.adapters.expression!.on('change', this.onUpdateData); + } + + componentWillUnmount() { + this.props.adapters.expression!.removeListener('change', this.onUpdateData); + } + + static renderNoData() { + return ( + + + + } + body={ + +

+ +

+
+ } + /> + ); + } + + render() { + if (!this.state.ast) { + return ExpressionsInspectorViewComponent.renderNoData(); + } + + return ; + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default ExpressionsInspectorViewComponent; diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx new file mode 100644 index 0000000000000..b10c82e5df309 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { lazy } from 'react'; + +const ExpressionsInspectorViewComponent = lazy(() => import('./expressions_inspector_view')); + +export const getExpressionsInspectorViewComponentWrapper = () => { + return (props: any) => { + return ; + }; +}; diff --git a/examples/expressions_explorer/public/inspector/index.ts b/examples/expressions_explorer/public/inspector/index.ts new file mode 100644 index 0000000000000..ec87a1240ac74 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Adapters, InspectorViewDescription } from '../../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewComponentWrapper } from './expressions_inspector_view_wrapper'; + +export const getExpressionsInspectorViewDescription = (): InspectorViewDescription => ({ + title: i18n.translate('data.inspector.table.dataTitle', { + defaultMessage: 'Expression', + }), + order: 100, + help: i18n.translate('data.inspector.table..dataDescriptionTooltip', { + defaultMessage: 'View the expression behind the visualization', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.expression); + }, + component: getExpressionsInspectorViewComponentWrapper(), +}); diff --git a/examples/expressions_explorer/public/plugin.tsx b/examples/expressions_explorer/public/plugin.tsx new file mode 100644 index 0000000000000..9643389ad881c --- /dev/null +++ b/examples/expressions_explorer/public/plugin.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewDescription } from './inspector'; +import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger'; +import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action'; +import { buttonRenderer } from './renderers/button'; +import { buttonFn } from './functions/button'; + +interface StartDeps { + expressions: ExpressionsStart; + inspector: InspectorStart; + uiActions: UiActionsStart; +} + +interface SetupDeps { + uiActions: UiActionsSetup; + expressions: ExpressionsSetup; + inspector: InspectorSetup; + developerExamples: DeveloperExamplesSetup; +} + +export class ExpressionsExplorerPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + // register custom inspector adapter & view + deps.inspector.registerView(getExpressionsInspectorViewDescription()); + + // register custom actions + deps.uiActions.registerTrigger(navigateTrigger); + deps.uiActions.registerAction(createNavigateAction()); + deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE); + + // register custom functions and renderers + deps.expressions.registerRenderer(buttonRenderer); + deps.expressions.registerFunction(buttonFn); + + core.application.register({ + id: 'expressionsExplorer', + title: 'Expressions Explorer', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const [, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp( + { + expressions: depsStart.expressions, + inspector: depsStart.inspector, + actions: depsStart.uiActions, + }, + params + ); + }, + }); + + deps.developerExamples.register({ + appId: 'expressionsExplorer', + title: 'Expressions', + description: `Expressions is a plugin that allows to execute Kibana expressions and render content using expression renderers. This example plugin showcases various usage scenarios.`, + links: [ + { + label: 'README', + href: 'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.md', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx new file mode 100644 index 0000000000000..ffbe558f30218 --- /dev/null +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RenderExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"'); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + return ( + + + + +

Render expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and render it. Using + ReactExpressionRenderer component makes that very easy. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+
+ ); +} diff --git a/examples/expressions_explorer/public/renderers/button.tsx b/examples/expressions_explorer/public/renderers/button.tsx new file mode 100644 index 0000000000000..32f1f31894dce --- /dev/null +++ b/examples/expressions_explorer/public/renderers/button.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import ReactDOM from 'react-dom'; +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { ExpressionRenderDefinition } from '../../../../src/plugins/expressions/common/expression_renderers'; + +export const buttonRenderer: ExpressionRenderDefinition = { + name: 'button', + displayName: 'Button', + reuseDomNode: true, + render(domNode, config, handlers) { + const buttonClick = () => { + handlers.event({ + id: 'NAVIGATE', + value: { + href: config.href, + }, + }); + }; + + const renderDebug = () => ( +
+ + {config.name} + +
+ ); + + ReactDOM.render(renderDebug(), domNode, () => handlers.done()); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}; diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx new file mode 100644 index 0000000000000..efbdbc2d41836 --- /dev/null +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { + EuiCodeBlock, + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RunExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer"'); + const [result, updateResult] = useState({}); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = useMemo( + () => ({ + expression: new ExpressionsInspectorAdapter(), + }), + [] + ); + + useEffect(() => { + const runExpression = async () => { + const execution = expressions.execute(expression, null, { + debug: true, + inspectorAdapters, + }); + + const data: any = await execution.getData(); + updateResult(data); + }; + + runExpression(); + }, [expression, expressions, inspectorAdapters]); + + return ( + + + + +

Run expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and execute it. Using + expressions.execute allows you to easily run the expression. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + + {JSON.stringify(result, null, '\t')} + + + + + + +
+ ); +} diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json new file mode 100644 index 0000000000000..b4449819b25a6 --- /dev/null +++ b/examples/expressions_explorer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 8e068818ec0ce..0240ec90cb1e6 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -29,6 +29,7 @@ import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; import { ExpressionExecutionParams } from '../service'; import { TablesAdapter } from '../util/tables_adapter'; +import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter'; /** * AbortController is not available in Node until v15, so we @@ -63,6 +64,7 @@ export interface ExecutionParams { const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ requests: new RequestAdapter(), tables: new TablesAdapter(), + expression: new ExpressionsInspectorAdapter(), }); export class Execution< @@ -208,6 +210,9 @@ export class Execution< this.firstResultFuture.promise .then( (result) => { + if (this.context.inspectorAdapters.expression) { + this.context.inspectorAdapters.expression.logAST(this.state.get().ast); + } this.state.transitions.setResult(result); }, (error) => { diff --git a/src/plugins/expressions/common/util/expressions_inspector_adapter.ts b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts new file mode 100644 index 0000000000000..c82884d373d2f --- /dev/null +++ b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { EventEmitter } from 'events'; + +export class ExpressionsInspectorAdapter extends EventEmitter { + private _ast: any = {}; + + public logAST(ast: any): void { + this._ast = ast; + this.emit('change', this._ast); + } + + public get ast() { + return this._ast; + } +} diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index ecb7d5cdca81e..4762f9979fe4a 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -9,3 +9,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; +export * from './expressions_inspector_adapter'; diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 9485daf49c981..d6dd2fc1f3d37 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -107,4 +107,5 @@ export { ExpressionsServiceSetup, ExpressionsServiceStart, TablesAdapter, + ExpressionsInspectorAdapter, } from '../common'; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 7fa0857be8aba..029d727e82e74 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -551,6 +551,16 @@ export class ExpressionRenderHandler { update$: Observable; } +// Warning: (ae-missing-release-tag) "ExpressionsInspectorAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ExpressionsInspectorAdapter extends EventEmitter { + // (undocumented) + get ast(): any; + // (undocumented) + logAST(ast: any): void; +} + // Warning: (ae-missing-release-tag) "ExpressionsPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/test/examples/config.js b/test/examples/config.js index a720899a637de..aab71cb305016 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -19,6 +19,7 @@ export default async function ({ readConfigFile }) { require.resolve('./ui_actions'), require.resolve('./state_sync'), require.resolve('./routing'), + require.resolve('./expressions_explorer'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts new file mode 100644 index 0000000000000..7261564e6db38 --- /dev/null +++ b/test/examples/expressions_explorer/expressions.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); + + describe('', () => { + it('runs expression', async () => { + await retry.try(async () => { + const text = await testSubjects.getVisibleText('expressionResult'); + expect(text).to.be( + '{\n "type": "error",\n "error": {\n "message": "Function markdown could not be found.",\n "name": "fn not found"\n }\n}' + ); + }); + }); + + it('renders expression', async () => { + await retry.try(async () => { + const text = await testSubjects.getVisibleText('expressionRender'); + expect(text).to.be('Function markdown could not be found.'); + }); + }); + + it('emits an action and navigates', async () => { + await testSubjects.click('testExpressionButton'); + await retry.try(async () => { + const text = await browser.getCurrentUrl(); + expect(text).to.be('https://www.google.com/?gws_rd=ssl'); + }); + }); + }); +} diff --git a/test/examples/expressions_explorer/index.ts b/test/examples/expressions_explorer/index.ts new file mode 100644 index 0000000000000..77d2a594c0f29 --- /dev/null +++ b/test/examples/expressions_explorer/index.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('expressions explorer', function () { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('expressionsExplorer'); + }); + + loadTestFile(require.resolve('./expressions')); + }); +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx index b4fbba96e8dfb..341913a033c05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx @@ -26,9 +26,11 @@ export const debug: RendererFactory = () => ({ ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - handlers.onResize(() => { - ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - }); + if (handlers.onResize) { + handlers.onResize(() => { + ReactDOM.render(renderDebug(), domNode, () => handlers.done()); + }); + } handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, From 8263d47d378315bdcad990620010c8452f3133d1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 22 Jan 2021 14:19:09 -0500 Subject: [PATCH 14/60] Fix sharing saved objects phase 2 CI (#89056) --- .../migrations/core/document_migrator.test.ts | 14 ++++++++++++++ .../migrations/core/document_migrator.ts | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 741f715ba6ebe..6ba652abda3d5 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -206,6 +206,20 @@ describe('DocumentMigrator', () => { ); }); + it('coerces the current Kibana version if it has a hyphen', () => { + const validDefinition = { + kibanaVersion: '3.2.0-SNAPSHOT', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.2.0', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(validDefinition)).not.toThrowError(); + }); + it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => { const invalidDefinition = { kibanaVersion: '3.2.3', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index e4b89a949d3cf..e93586ec7ce4c 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -159,10 +159,11 @@ export class DocumentMigrator implements VersionedTransformer { */ constructor({ typeRegistry, - kibanaVersion, + kibanaVersion: rawKibanaVersion, minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION, log, }: DocumentMigratorOptions) { + const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z) validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion); this.documentMigratorOptions = { typeRegistry, kibanaVersion, log }; From c739f437ddcf48af1fb5719f0034fe1ef45f31b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 10:36:21 +0200 Subject: [PATCH 15/60] Update dependency vega to ^5.19.1 (#88984) Co-authored-by: Renovate Bot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 60 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 87e0f84695235..2fdc31820b9d4 100644 --- a/package.json +++ b/package.json @@ -828,7 +828,7 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.18.0", + "vega": "^5.19.1", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", diff --git a/yarn.lock b/yarn.lock index cc32349b10860..828a3b630a838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28645,10 +28645,10 @@ vega-functions@^5.10.0: vega-time "^2.0.4" vega-util "^1.16.0" -vega-functions@~5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.11.0.tgz#a590d016f93c81730bdbc336b377231d7ae48569" - integrity sha512-/p0QIDiA3RaUZ7drxHuClpDQCrIScSHJlY0oo0+GFYGfp+lvb29Ox1T4a+wtqeCp6NRaTWry+EwDxojnshTZIQ== +vega-functions@^5.12.0, vega-functions@~5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" + integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== dependencies: d3-array "^2.7.1" d3-color "^2.0.0" @@ -28656,8 +28656,8 @@ vega-functions@~5.11.0: vega-dataflow "^5.7.3" vega-expression "^4.0.1" vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.2.0" + vega-scenegraph "^4.9.3" + vega-selections "^5.3.0" vega-statistics "^1.7.9" vega-time "^2.0.4" vega-util "^1.16.0" @@ -28724,16 +28724,16 @@ vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: vega-format "^1.0.4" vega-util "^1.16.0" -vega-parser@~6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.2.tgz#7f25751177e38c3239560a9c427ded8d2ba617bb" - integrity sha512-aGyZrNzPrBruEb/WhemKDuDjQsIkMDGIgnSJci0b+9ZVxjyAzMl7UfGbiYorPiJlnIercjUJbMoFD6fCIf4gqQ== +vega-parser@~6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.3.tgz#df72785e4b086eceb90ee6219a399210933b507b" + integrity sha512-8oiVhhW26GQ4GZBvolId8FVFvhn3s1KGgPlD7Z+4P2wkV+xe5Nqu0TEJ20F/cn3b88fd0Vj48X3BH3dlSeKNFg== dependencies: vega-dataflow "^5.7.3" vega-event-selector "^2.0.6" - vega-functions "^5.10.0" + vega-functions "^5.12.0" vega-scale "^7.1.1" - vega-util "^1.15.2" + vega-util "^1.16.0" vega-projection@^1.4.5, vega-projection@~1.4.5: version "1.4.5" @@ -28772,7 +28772,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: +vega-scenegraph@^4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== @@ -28784,6 +28784,18 @@ vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: vega-scale "^7.1.1" vega-util "^1.15.2" +vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" + integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== + dependencies: + d3-path "^2.0.0" + d3-shape "^2.0.0" + vega-canvas "^1.2.5" + vega-loader "^4.3.3" + vega-scale "^7.1.1" + vega-util "^1.15.2" + vega-schema-url-parser@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" @@ -28797,10 +28809,10 @@ vega-selections@^5.1.5: vega-expression "^4.0.0" vega-util "^1.15.2" -vega-selections@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.2.0.tgz#d85968d1bccc175fd92661c91d88151ffd5ade83" - integrity sha512-Xf3nTTJHRGw4tQMbt+0sBI/7WkEIzPG9E4HXkZk5Y9Q2HsGRVLmrAEXHSfpENrBLWTBZk/uvmP9rKDG7cbcTrg== +vega-selections@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" + integrity sha512-vC4NPsuN+IffruFXfH0L3i2A51RgG4PqpLv85TvrEAIYnSkyKDE4bf+wVraR3aPdnLLkc3+tYuMi6le5FmThIA== dependencies: vega-expression "^4.0.1" vega-util "^1.16.0" @@ -28899,10 +28911,10 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.18.0: - version "5.18.0" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.18.0.tgz#98645e5d3bd5267d66ea3e701d99dcff63cfff8a" - integrity sha512-ysqouhboWNXSuQNN7W5IGOXsnEJNFVX5duCi0tTwRsFLc61FshpqVh4+4VoXg5pH0ZCxwpqbOwd2ULZWjJTx6g== +vega@^5.19.1: + version "5.19.1" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.19.1.tgz#64c8350740fe1a11d56cc6617ab3a76811fd704c" + integrity sha512-UE6/c9q9kzuz4HULFuU9HscBASoZa+zcXqGKdbQP545Nwmhd078QpcH+wZsq9lYfiTxmFtzLK/a0OH0zhkghvA== dependencies: vega-crossfilter "~4.0.5" vega-dataflow "~5.7.3" @@ -28911,17 +28923,17 @@ vega@^5.18.0: vega-expression "~4.0.1" vega-force "~4.0.7" vega-format "~1.0.4" - vega-functions "~5.11.0" + vega-functions "~5.12.0" vega-geo "~4.3.8" vega-hierarchy "~4.0.9" vega-label "~1.0.0" vega-loader "~4.4.0" - vega-parser "~6.1.2" + vega-parser "~6.1.3" vega-projection "~1.4.5" vega-regression "~1.0.9" vega-runtime "~6.1.3" vega-scale "~7.1.1" - vega-scenegraph "~4.9.2" + vega-scenegraph "~4.9.3" vega-statistics "~1.7.9" vega-time "~2.0.4" vega-transforms "~4.9.3" From 02f24de89994c34dcc75ba30c44c56731ffdbe13 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 25 Jan 2021 11:25:46 +0100 Subject: [PATCH 16/60] [Lens] Add drag drop tests (#88660) --- .../functional/apps/lens/drag_and_drop.ts | 153 +++-- .../es_archives/lens/basic/data.json | 577 ++++++++++++++++++ .../test/functional/page_objects/lens_page.ts | 3 + 3 files changed, 674 insertions(+), 59 deletions(-) create mode 100644 x-pack/test/functional/es_archives/lens/basic/data.json diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index e0130bc394271..57e5990a74012 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -11,73 +11,108 @@ export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); describe('lens drag and drop tests', () => { - it('should construct the basic split xy chart', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickVisType('lens'); - await PageObjects.lens.goToTimeRange(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + describe('basic drag and drop', () => { + it('should construct the basic split xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); - expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( - '@timestamp' - ); - }); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + }); - it('should allow dropping fields to existing and empty dimension triggers', async () => { - await PageObjects.lens.switchToVisualization('lnsDatatable'); + it('should allow dropping fields to existing and empty dimension triggers', async () => { + await PageObjects.lens.switchToVisualization('lnsDatatable'); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'clientip', - 'lnsDatatable_column > lns-dimensionTrigger' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( - 'Top values of clientip' - ); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'clientip', + 'lnsDatatable_column > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( + 'Top values of clientip' + ); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'bytes', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( - 'bytes' - ); - await PageObjects.lens.dragFieldToDimensionTrigger( - '@message.raw', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( - 'Top values of @message.raw' - ); - }); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( + 'bytes' + ); + await PageObjects.lens.dragFieldToDimensionTrigger( + '@message.raw', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( + 'Top values of @message.raw' + ); + }); - it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); - await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ - 'Top values of @message.raw', - 'Top values of clientip', - 'bytes', - ]); - }); + it('should reorder the elements for the table', async () => { + await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ + 'Top values of @message.raw', + 'Top values of clientip', + 'bytes', + ]); + }); - it('should move the column to compatible dimension group', async () => { - await PageObjects.lens.switchToVisualization('bar'); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of clientip', - ]); + it('should move the column to compatible dimension group', async () => { + await PageObjects.lens.switchToVisualization('bar'); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'Top values of @message.raw', + ]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_xDimensionPanel > lns-dimensionTrigger', + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( + [] + ); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of @message.raw']); + }); + }); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xDimensionPanel > lns-dimensionTrigger', - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' - ); + describe('workspace drop', () => { + it('should always nest time dimension in categorical dimension', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + await PageObjects.lens.openDimensionEditor( + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.isTopLevelAggregation()).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + }); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); + it('overwrite existing time dimension if one exists already', async () => { + await PageObjects.lens.dragFieldToWorkspace('utc_time'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'utc_time', + ]); + }); }); }); } diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json b/x-pack/test/functional/es_archives/lens/basic/data.json new file mode 100644 index 0000000000000..a985de882929d --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/basic/data.json @@ -0,0 +1,577 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [], + "space": { + "_reserved": true, + "description": "This is the default space!", + "disabledFeatures": [], + "name": "Default" + }, + "type": "space" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "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-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:log*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "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": "log*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "custom_space:index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "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-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "namespace": "custom_space", + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "custom_space:visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "namespace": "custom_space", + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "query:okjpgs", + "index": ".kibana_1", + "source": { + "query": { + "description": "Ok responses for jpg files", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": null, + "disabled": false, + "index": "b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b", + "key": "extension.raw", + "negate": false, + "params": { + "query": "jpg" + }, + "type": "phrase", + "value": "jpg" + }, + "query": { + "match": { + "extension.raw": { + "query": "jpg", + "type": "phrase" + } + } + } + } + ], + "query": { + "language": "kuery", + "query": "response:200" + }, + "title": "OKJpgs" + }, + "references": [], + "type": "query", + "updated_at": "2019-07-17T17:54:26.378Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "logstash-*" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-04T18:47:24.761Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac1", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"c61a8afb-a185-4fae-a064-fb3846f6c451\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"max\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\"}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-2cd09808-3915-49f4-b3b0-82767eba23f7\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Maximum of bytes\\\",\\\"operationType\\\":\\\"max\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\"}}\"}\n| lens_metric_chart title=\"Maximum of bytes\" accessor=\"2cd09808-3915-49f4-b3b0-82767eba23f7\" mode=\"full\"", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "c61a8afb-a185-4fae-a064-fb3846f6c451": { + "columnOrder": [ + "2cd09808-3915-49f4-b3b0-82767eba23f7" + ], + "columns": { + "2cd09808-3915-49f4-b3b0-82767eba23f7": { + "dataType": "number", + "isBucketed": false, + "label": "Maximum of bytes", + "operationType": "max", + "scale": "ratio", + "sourceField": "bytes" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7", + "isHorizontal": false, + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "layers": [ + { + "accessors": [ + "d3e62a7a-c259-4fff-a2fc-eebf20b7008a", + "26ef70a9-c837-444c-886e-6bd905ee7335" + ], + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "seriesType": "area", + "splitAccessor": "54cd64ed-2a44-4591-af84-b2624504569a", + "xAccessor": "d6e40cea-6299-43b4-9c9d-b4ee305a2ce8" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "area" + } + }, + "title": "Artistpreviouslyknownaslens", + "visualizationType": "lnsMetric" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:9536bed0-d57e-11ea-b169-e3a222a76b9c", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs=\"[{\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.dest\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":7,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.src\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\" | lens_rename_columns idMap=\"{\\\"col-0-bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\":{\\\"dataType\\\":\\\"string\\\",\\\"isBucketed\\\":true,\\\"label\\\":\\\"Top values of geo.dest\\\",\\\"operationType\\\":\\\"terms\\\",\\\"params\\\":{\\\"orderBy\\\":{\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"type\\\":\\\"column\\\"},\\\"orderDirection\\\":\\\"desc\\\",\\\"size\\\":7},\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.dest\\\",\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\"},\\\"col-2-c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\":{\\\"label\\\":\\\"Top values of geo.src\\\",\\\"dataType\\\":\\\"string\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.src\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\"},\\\"col-3-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Average of bytes\\\",\\\"operationType\\\":\\\"avg\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_pie shape=\"pie\" hideLabels=false groups=\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\"\n groups=\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\" metric=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" numberDisplay=\"percent\" categoryDisplay=\"default\" legendDisplay=\"default\" legendPosition=\"right\" percentDecimals=3 nestedLegend=false", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "5bd1c078-e1dd-465b-8d25-7a6404befa88": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp" + }, + "65340cf3-8402-4494-96f2-293701c59571": { + "dataType": "number", + "isBucketed": true, + "label": "Top values of bytes", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "bytes" + }, + "87554e1d-3dbf-4c1c-a358-4c9d40424cfa": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of type", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "type" + }, + "bafe3009-1776-4227-a0fe-b0d6ccbb4961": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.dest", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 7 + }, + "scale": "ordinal", + "sourceField": "geo.dest" + }, + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "geo.src" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "categoryDisplay": "default", + "groups": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "legendDisplay": "default", + "metric": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "nestedLegend": false, + "numberDisplay": "percent" + } + ], + "shape": "pie" + } + }, + "title": "lnsPieVis", + "visualizationType": "lnsPie" + }, + "references": [], + "type": "lens", + "updated_at": "2020-08-03T11:43:43.421Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac2", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"ip\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"date_histogram\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"@timestamp\\\",\\\"useNormalizedEsInterval\\\":true,\\\"interval\\\":\\\"auto\\\",\\\"drop_partials\\\":false,\\\"min_doc_count\\\":0,\\\"extended_bounds\\\":{}}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":{\\\"label\\\":\\\"Top values of ip\\\",\\\"dataType\\\":\\\"ip\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"suggestedPriority\\\":0,\\\"sourceField\\\":\\\"ip\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\"},\\\"col-1-3cf18f28-3495-4d45-a55f-d97f88022099\\\":{\\\"label\\\":\\\"@timestamp\\\",\\\"dataType\\\":\\\"date\\\",\\\"operationType\\\":\\\"date_histogram\\\",\\\"suggestedPriority\\\":1,\\\"sourceField\\\":\\\"@timestamp\\\",\\\"isBucketed\\\":true,\\\"scale\\\":\\\"interval\\\",\\\"params\\\":{\\\"interval\\\":\\\"auto\\\"},\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\"},\\\"col-2-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"label\\\":\\\"Average of bytes\\\",\\\"dataType\\\":\\\"number\\\",\\\"operationType\\\":\\\"avg\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"isBucketed\\\":false,\\\"scale\\\":\\\"ratio\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_xy_chart xTitle=\"@timestamp\" yTitle=\"Average of bytes\" legend={lens_xy_legendConfig isVisible=true position=\"right\"} \n layers={lens_xy_layer layerId=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" hide=false xAccessor=\"3cf18f28-3495-4d45-a55f-d97f88022099\" yScaleType=\"linear\" xScaleType=\"time\" isHistogram=true splitAccessor=\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\" seriesType=\"bar_stacked\" accessors=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" columnToLabel=\"{\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":\\\"Average of bytes\\\",\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":\\\"Top values of ip\\\"}\"}", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "3cf18f28-3495-4d45-a55f-d97f88022099", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3cf18f28-3495-4d45-a55f-d97f88022099": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp", + "suggestedPriority": 1 + }, + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3": { + "dataType": "ip", + "isBucketed": true, + "label": "Top values of ip", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "ip", + "suggestedPriority": 0 + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "accessors": [ + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "seriesType": "bar_stacked", + "splitAccessor": "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "xAccessor": "3cf18f28-3495-4d45-a55f-d97f88022099" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "bar_stacked" + } + }, + "title": "lnsXYvis", + "visualizationType": "lnsXY" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:DashboardPanelVersionInUrl:8.0.0", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2019-10-16T00:28:24.399Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 04c660847bcee..31a4d6e29fc35 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -241,6 +241,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async isTopLevelAggregation() { + return await testSubjects.isEuiSwitchChecked('indexPattern-nesting-switch'); + }, /** * Removes the dimension matching a specific test subject */ From 9d7cac76f5cfb72b9a937165ba53752e1e14c0c5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 25 Jan 2021 11:26:30 +0100 Subject: [PATCH 17/60] increase timeout on graph test for cloud (#88612) --- x-pack/test/functional/page_objects/graph_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index 9ce1f87b5bf3d..8dda82e4e08bb 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -196,7 +196,7 @@ export function GraphPageProvider({ getService, getPageObjects }: FtrProviderCon await testSubjects.click('confirmSaveSavedObjectButton'); // Confirm that the Graph has been saved. - return await testSubjects.exists('saveGraphSuccess'); + return await testSubjects.exists('saveGraphSuccess', { timeout: 10000 }); } async getSearchFilter() { From 403021fcabac0bf4416e3c2dd0735cd565068ec0 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 25 Jan 2021 11:28:10 +0100 Subject: [PATCH 18/60] [Search Sessions] omit searchSessionId from the initialState, explicitly pause refreshInterval in restoreState (#88650) --- .../lib/session_restoration.test.ts | 60 +++++++++++++++++++ .../application/lib/session_restoration.ts | 21 ++++--- .../angular/discover_state.test.ts | 29 ++++++++- .../application/angular/discover_state.ts | 15 ++--- 4 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 src/plugins/dashboard/public/application/lib/session_restoration.test.ts diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts new file mode 100644 index 0000000000000..56db5346b7c6c --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createSessionRestorationDataProvider } from './session_restoration'; +import { getAppStateDefaults } from './get_app_state_defaults'; +import { getSavedDashboardMock } from '../test_helpers'; +import { SavedObjectTagDecoratorTypeGuard } from '../../../../saved_objects_tagging_oss/public'; + +describe('createSessionRestorationDataProvider', () => { + const mockDataPlugin = dataPluginMock.createStartContract(); + const searchSessionInfoProvider = createSessionRestorationDataProvider({ + data: mockDataPlugin, + getAppState: () => + getAppStateDefaults( + getSavedDashboardMock(), + false, + ((() => false) as unknown) as SavedObjectTagDecoratorTypeGuard + ), + getDashboardTitle: () => 'Dashboard', + getDashboardId: () => 'Id', + }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + + test('restoreState has refreshInterval paused', async () => { + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.refreshInterval).toBeUndefined(); + expect(restoreState.refreshInterval?.pause).toBe(true); + }); + }); +}); diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 60a0c56a63218..fb57f8caa5ce4 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -21,8 +21,8 @@ export function createSessionRestorationDataProvider(deps: { getUrlGeneratorData: async () => { return { urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, - initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), - restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), }; }, }; @@ -32,20 +32,17 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,6 +52,12 @@ function getUrlGeneratorState({ preserveSavedFilters: false, viewMode: appState.viewMode, panels: getDashboardId() ? undefined : appState.panels, - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, + refreshInterval: shouldRestoreSearchSession + ? { + pause: true, // force pause refresh interval when restoring a session + value: 0, + } + : undefined, }; } diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index 45e5e252e8361..809664de5f073 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -101,8 +101,9 @@ describe('Test discover state with legacy migration', () => { describe('createSearchSessionRestorationDataProvider', () => { let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch; + const mockDataPlugin = dataPluginMock.createStartContract(); const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ - data: dataPluginMock.createStartContract(), + data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), }).appStateContainer, @@ -124,4 +125,30 @@ describe('createSearchSessionRestorationDataProvider', () => { expect(await searchSessionInfoProvider.getName()).toBe('Discover'); }); }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index fe05fceb858e5..c769e263655ab 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -275,12 +275,12 @@ export function createSearchSessionRestorationDataProvider(deps: { initialState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: false, + shouldRestoreSearchSession: false, }), restoreState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: true, + shouldRestoreSearchSession: true, }), }; }, @@ -291,15 +291,12 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); return { @@ -307,10 +304,10 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, columns: appState.columns, sort: appState.sort, savedQuery: appState.savedQuery, From 2ff523556d6b1cc38943369889dbbc3a5f2b6e7a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 25 Jan 2021 12:30:49 +0200 Subject: [PATCH 19/60] [XY Axis] Fix bug on percentiles and percentiles ranks (#88576) * [XY Axis] Fix bug on percentiles and percentiles ranks * Add unit tests to renderAllSeries * make it simpler * Minor change on test * Fix license headers Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../utils/render_all_series.test.mocks.ts | 386 ++++++++++++++++++ .../public/utils/render_all_series.test.tsx | 132 ++++++ .../public/utils/render_all_series.tsx | 18 +- .../vis_type_xy/public/vis_component.tsx | 45 +- 4 files changed, 560 insertions(+), 21 deletions(-) create mode 100644 src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts create mode 100644 src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts new file mode 100644 index 0000000000000..393a6ee06cf58 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts @@ -0,0 +1,386 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { VisConfig } from '../types'; + +export const getVisConfig = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3', + column: 1, + title: 'Average products.base_price', + format: { + id: 'number', + }, + aggType: 'avg', + aggId: '3', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getVisConfigPercentiles = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3.1', + column: 1, + title: '1st percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.1', + params: {}, + }, + { + accessor: 'col-2-3.5', + column: 2, + title: '5th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.5', + params: {}, + }, + { + accessor: 'col-3-3.25', + column: 3, + title: '25th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.25', + params: {}, + }, + { + accessor: 'col-4-3.50', + column: 4, + title: '50th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.50', + params: {}, + }, + { + accessor: 'col-5-3.75', + column: 5, + title: '75th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.75', + params: {}, + }, + { + accessor: 'col-6-3.95', + column: 6, + title: '95th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.95', + params: {}, + }, + { + accessor: 'col-7-3.99', + column: 7, + title: '99th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.99', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getPercentilesData = () => { + return [ + { + 'col-0-2': 1610961900000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 38.49609375, + 'col-5-3.75': 65, + 'col-6-3.95': 65, + 'col-7-3.99': 65, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3.1': 28.984375000000004, + 'col-2-3.5': 28.984375, + 'col-3-3.25': 28.984375, + 'col-4-3.50': 30.9921875, + 'col-5-3.75': 41.5, + 'col-6-3.95': 50, + 'col-7-3.99': 50, + }, + { + 'col-0-2': 1610963280000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 12.9921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + { + 'col-0-2': 1610964180000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 14.9921875, + 'col-4-3.50': 15.98828125, + 'col-5-3.75': 24.984375, + 'col-6-3.95': 85, + 'col-7-3.99': 85, + }, + { + 'col-0-2': 1610964420000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 23.99609375, + 'col-5-3.75': 42, + 'col-6-3.95': 42, + 'col-7-3.99': 42, + }, + { + 'col-0-2': 1610964600000, + 'col-1-3.1': 10.9921875, + 'col-2-3.5': 10.992187500000002, + 'col-3-3.25': 10.9921875, + 'col-4-3.50': 12.4921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + ]; +}; diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx new file mode 100644 index 0000000000000..d76ea49a2f110 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { AreaSeries, BarSeries, CurveType } from '@elastic/charts'; +import { DatatableRow } from '../../../expressions/public'; +import { renderAllSeries } from './render_all_series'; +import { + getVisConfig, + getVisConfigPercentiles, + getPercentilesData, +} from './render_all_series.test.mocks'; +import { SeriesParam, VisConfig } from '../types'; + +const defaultSeriesParams = [ + { + data: { + id: '3', + label: 'Label', + }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + lineWidth: 2, + mode: 'stacked', + show: true, + showCircles: true, + type: 'area', + valueAxis: 'ValueAxis-1', + }, +] as SeriesParam[]; + +const defaultData = [ + { + 'col-0-2': 1610960220000, + 'col-1-3': 26.984375, + }, + { + 'col-0-2': 1610961300000, + 'col-1-3': 30.99609375, + }, + { + 'col-0-2': 1610961900000, + 'col-1-3': 38.49609375, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3': 35.2421875, + }, +]; + +describe('renderAllSeries', function () { + const getAllSeries = (visConfig: VisConfig, params: SeriesParam[], data: DatatableRow[]) => { + return renderAllSeries( + visConfig, + params, + data, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + }; + + it('renders an area Series and not a bar series if type is area', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(1); + expect(wrapper.find(BarSeries).length).toBe(0); + }); + + it('renders a bar Series in case of histogram', () => { + const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram' }]; + + const renderBarSeries = renderAllSeries( + getVisConfig(), + barSeriesParams as SeriesParam[], + defaultData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderBarSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(0); + expect(wrapper.find(BarSeries).length).toBe(1); + }); + + it('renders the correct yAccessors for not percentile aggs', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual(['col-1-3']); + }); + + it('renders the correct yAccessors for percentile aggs', () => { + const percentilesConfig = getVisConfigPercentiles(); + const percentilesData = getPercentilesData(); + const renderPercentileSeries = renderAllSeries( + percentilesConfig, + defaultSeriesParams as SeriesParam[], + percentilesData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderPercentileSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual([ + 'col-1-3.1', + 'col-2-3.5', + 'col-3-3.25', + 'col-4-3.50', + 'col-5-3.75', + 'col-6-3.95', + 'col-7-3.99', + ]); + }); + + it('defaults the CurveType to linear', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('curve')).toEqual(CurveType.LINEAR); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx index 264fa539c1980..fb884bb235971 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx @@ -71,13 +71,15 @@ export const renderAllSeries = ( interpolate, type, }) => { - const yAspect = aspects.y.find(({ aggId }) => aggId === paramId); - - if (!show || !yAspect || yAspect.accessor === null) { + const yAspects = aspects.y.filter( + ({ aggId, accessor }) => aggId?.includes(paramId) && accessor !== null + ); + if (!show || !yAspects.length) { return null; } + const yAccessors = yAspects.map((aspect) => aspect.accessor) as string[]; - const id = `${type}-${yAspect.accessor}`; + const id = `${type}-${yAccessors[0]}`; const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; const isStacked = mode === 'stacked' || yAxisScale?.mode === 'percentage'; const stackMode = yAxisScale?.mode === 'normal' ? undefined : yAxisScale?.mode; @@ -94,13 +96,13 @@ export const renderAllSeries = ( id={id} name={getSeriesName} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} groupId={pseudoGroupId} useDefaultGroupDomain={useDefaultGroupDomain} xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} splitSeriesAccessors={splitSeriesAccessors} data={data} timeZone={timeZone} @@ -125,7 +127,7 @@ export const renderAllSeries = ( id={id} fit={fittingFunction} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} name={getSeriesName} curve={getCurveType(interpolate)} groupId={pseudoGroupId} @@ -133,7 +135,7 @@ export const renderAllSeries = ( xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} markSizeAccessor={markSizeAccessor} markFormat={aspects.z?.formatter} splitSeriesAccessors={splitSeriesAccessors} diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 0cdabd2fa409e..6f994707cbb72 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -296,10 +296,38 @@ const VisComponent = (props: VisComponentProps) => { ] ); const xAccessor = getXAccessor(config.aspects.x); - const splitSeriesAccessors = config.aspects.series - ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) - : []; + const splitSeriesAccessors = useMemo( + () => + config.aspects.series + ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) + : [], + [config.aspects.series] + ); + + const renderSeries = useMemo( + () => + renderAllSeries( + config, + visParams.seriesParams, + visData.rows, + getSeriesName, + getSeriesColor, + timeZone, + xAccessor, + splitSeriesAccessors + ), + [ + config, + getSeriesColor, + getSeriesName, + splitSeriesAccessors, + timeZone, + visData.rows, + visParams.seriesParams, + xAccessor, + ] + ); return (
{ {config.yAxes.map((axisProps) => ( ))} - {renderAllSeries( - config, - visParams.seriesParams, - visData.rows, - getSeriesName, - getSeriesColor, - timeZone, - xAccessor, - splitSeriesAccessors - )} + {renderSeries}
); From 72ef3b105a38ae66045849054adc7408daee163c Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 25 Jan 2021 14:11:59 +0300 Subject: [PATCH 20/60] [Data table] Add telemetry for table vis split mode (#88604) * Add telemetry for table vis * Update telemetry schema * Add unit tests * Update license * Use soClient instead of esClient, update tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/telemetry/schema/oss_plugins.json | 30 +++++++ src/plugins/vis_type_table/common/index.ts | 9 ++ src/plugins/vis_type_table/common/types.ts | 28 ++++++ src/plugins/vis_type_table/jest.config.js | 1 + .../public/components/table_vis_options.tsx | 2 +- .../components/table_vis_options_lazy.tsx | 2 +- .../vis_type_table/public/components/utils.ts | 2 +- .../public/legacy/table_vis_legacy_fn.ts | 5 +- .../public/legacy/table_vis_legacy_type.ts | 4 +- .../vis_type_table/public/table_vis_fn.ts | 5 +- .../vis_type_table/public/table_vis_type.ts | 4 +- .../vis_type_table/public/to_ast.test.ts | 2 +- src/plugins/vis_type_table/public/to_ast.ts | 3 +- src/plugins/vis_type_table/public/types.ts | 19 +---- .../public/utils/use/use_formatted_columns.ts | 3 +- .../public/utils/use/use_pagination.ts | 2 +- src/plugins/vis_type_table/server/index.ts | 10 ++- .../server/usage_collector/get_stats.test.ts | 67 +++++++++++++++ .../server/usage_collector/get_stats.ts | 85 +++++++++++++++++++ .../server/usage_collector/index.ts | 9 ++ .../register_usage_collector.test.ts | 56 ++++++++++++ .../register_usage_collector.ts | 32 +++++++ src/plugins/vis_type_table/tsconfig.json | 1 + src/plugins/visualizations/common/index.ts | 10 +++ src/plugins/visualizations/common/types.ts | 32 +++++++ src/plugins/visualizations/public/index.ts | 5 +- .../public/legacy/vis_update_state.d.ts | 2 +- .../saved_visualization_references.test.ts | 3 +- src/plugins/visualizations/public/types.ts | 16 +--- src/plugins/visualizations/public/vis.ts | 5 +- 30 files changed, 398 insertions(+), 56 deletions(-) create mode 100644 src/plugins/vis_type_table/common/index.ts create mode 100644 src/plugins/vis_type_table/common/types.ts create mode 100644 src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts create mode 100644 src/plugins/vis_type_table/server/usage_collector/get_stats.ts create mode 100644 src/plugins/vis_type_table/server/usage_collector/index.ts create mode 100644 src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts create mode 100644 src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts create mode 100644 src/plugins/visualizations/common/index.ts create mode 100644 src/plugins/visualizations/common/types.ts diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 50a08d96de951..27d9b5ce83203 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -5247,6 +5247,36 @@ } } }, + "vis_type_table": { + "properties": { + "total": { + "type": "long" + }, + "total_split": { + "type": "long" + }, + "split_columns": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + }, + "split_rows": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + } + } + }, "vis_type_vega": { "properties": { "vega_lib_specs_total": { diff --git a/src/plugins/vis_type_table/common/index.ts b/src/plugins/vis_type_table/common/index.ts new file mode 100644 index 0000000000000..cc54db82d37e7 --- /dev/null +++ b/src/plugins/vis_type_table/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './types'; diff --git a/src/plugins/vis_type_table/common/types.ts b/src/plugins/vis_type_table/common/types.ts new file mode 100644 index 0000000000000..3380e730770c3 --- /dev/null +++ b/src/plugins/vis_type_table/common/types.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export const VIS_TYPE_TABLE = 'table'; + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface TableVisParams { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + showToolbar: boolean; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; + row?: boolean; +} diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js index 4e5ddbcf8d7c5..3a7906f6ec543 100644 --- a/src/plugins/vis_type_table/jest.config.js +++ b/src/plugins/vis_type_table/jest.config.js @@ -11,4 +11,5 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/vis_type_table'], testRunner: 'jasmine2', + collectCoverageFrom: ['/src/plugins/vis_type_table/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index eb76659a601d6..a70ecb43f1be7 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -19,7 +19,7 @@ import { NumberInputOption, VisOptionsProps, } from '../../../vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; import { totalAggregations } from './utils'; const { tabifyGetColumns } = search; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx index fb0044a986f5e..716b77e9c91d2 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx @@ -9,7 +9,7 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; const TableOptionsComponent = lazy(() => import('./table_vis_options')); diff --git a/src/plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts index f11d7bc4b7f33..8f30788c76468 100644 --- a/src/plugins/vis_type_table/public/components/utils.ts +++ b/src/plugins/vis_type_table/public/components/utils.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AggTypes } from '../types'; +import { AggTypes } from '../../common'; const totalAggregations = [ { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts index cec16eefb360c..db0b92154d2dd 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public'; import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler'; import { TableVisConfig } from '../types'; +import { VIS_TYPE_TABLE } from '../../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -53,7 +54,7 @@ export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => ( as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index a1ceee8c741d4..3e1140275593d 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -12,11 +12,11 @@ import { BaseVisTypeOptions } from '../../../visualizations/public'; import { TableOptions } from '../components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; import { toExpressionAst } from '../to_ast'; -import { TableVisParams } from '../types'; export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index a45f1e828fc47..99fee424b8bea 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TableVisConfig } from './types'; +import { VIS_TYPE_TABLE } from '../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -56,7 +57,7 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 8cd45b54c6ced..ef6d85db103b3 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../common'; import { TableOptions } from './components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { TableVisParams } from './types'; export const tableVisTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts index 1ca62475b7af0..f0aed7199a2f2 100644 --- a/src/plugins/vis_type_table/public/to_ast.test.ts +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -8,7 +8,7 @@ import { Vis } from 'src/plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { AggTypes, TableVisParams } from './types'; +import { AggTypes, TableVisParams } from '../common'; const mockSchemas = { metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts index 9d9f23d31d802..1cbe9832e4c98 100644 --- a/src/plugins/vis_type_table/public/to_ast.ts +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -12,8 +12,9 @@ import { } from '../../data/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TableVisParams } from '../common'; import { TableExpressionFunctionDefinition } from './table_vis_fn'; -import { TableVisConfig, TableVisParams } from './types'; +import { TableVisConfig } from './types'; const buildTableVisConfig = ( schemas: ReturnType, diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 75d48f4f53ac7..03cf8bb3395d6 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -8,14 +8,7 @@ import { IFieldFormat } from 'src/plugins/data/public'; import { SchemaConfig } from 'src/plugins/visualizations/public'; - -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} +import { TableVisParams } from '../common'; export interface Dimensions { buckets: SchemaConfig[]; @@ -44,16 +37,6 @@ export interface TableVisUseUiStateProps { setColumnsWidth: (column: ColumnWidthData) => void; } -export interface TableVisParams { - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; - showToolbar: boolean; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; -} - export interface TableVisConfig extends TableVisParams { title: string; dimensions: Dimensions; diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts index 5398aa908f6eb..3a733e7a9a4dc 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts @@ -9,8 +9,9 @@ import { useMemo } from 'react'; import { chain, findIndex } from 'lodash'; +import { AggTypes } from '../../../common'; import { Table } from '../../table_vis_response_handler'; -import { FormattedColumn, TableVisConfig, AggTypes } from '../../types'; +import { FormattedColumn, TableVisConfig } from '../../types'; import { getFormatService } from '../../services'; import { addPercentageColumn } from '../add_percentage_column'; diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts index 1573a3c6b7b88..7e55e63f9249c 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts @@ -7,7 +7,7 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableVisParams } from '../../types'; +import { TableVisParams } from '../../../common'; export const usePagination = (visParams: TableVisParams, rowCount: number) => { const [pagination, setPagination] = useState({ diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts index 75068c646f501..39618d687168e 100644 --- a/src/plugins/vis_type_table/server/index.ts +++ b/src/plugins/vis_type_table/server/index.ts @@ -6,9 +6,11 @@ * Public License, v 1. */ -import { PluginConfigDescriptor } from 'kibana/server'; +import { CoreSetup, PluginConfigDescriptor } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { configSchema, ConfigSchema } from '../config'; +import { registerVisTypeTableUsageCollector } from './usage_collector'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -21,6 +23,10 @@ export const config: PluginConfigDescriptor = { }; export const plugin = () => ({ - setup() {}, + setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + if (plugins.usageCollection) { + registerVisTypeTableUsageCollector(plugins.usageCollection); + } + }, start() {}, }); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts new file mode 100644 index 0000000000000..55daa5c64349a --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { getStats } from './get_stats'; + +const mockVisualizations = { + saved_objects: [ + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": true }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": false }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "split", "enabled": true }], "params": { "row": false }}', + }, + }, + { + attributes: { + visState: '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }]}', + }, + }, + { + attributes: { visState: '{"type": "histogram"}' }, + }, + ], +}; + +describe('vis_type_table getStats', () => { + const mockSoClient = ({ + find: jest.fn().mockResolvedValue(mockVisualizations), + } as unknown) as SavedObjectsClientContract; + + test('Returns stats from saved objects for table vis only', async () => { + const result = await getStats(mockSoClient); + expect(mockSoClient.find).toHaveBeenCalledWith({ + type: 'visualization', + perPage: 10000, + }); + expect(result).toEqual({ + total: 4, + total_split: 3, + split_columns: { + total: 1, + enabled: 1, + }, + split_rows: { + total: 2, + enabled: 1, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts new file mode 100644 index 0000000000000..bd3e1d2f089e2 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; +import { + SavedVisState, + VisualizationSavedObjectAttributes, +} from 'src/plugins/visualizations/common'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; + +export interface VisTypeTableUsage { + /** + * Total number of table type visualizations + */ + total: number; + /** + * Total number of table visualizations, using "Split table" agg + */ + total_split: number; + /** + * Split table by columns stats + */ + split_columns: { + total: number; + enabled: number; + }; + /** + * Split table by rows stats + */ + split_rows: { + total: number; + enabled: number; + }; +} + +/* + * Parse the response data into telemetry payload + */ +export async function getStats( + soClient: SavedObjectsClientContract | ISavedObjectsRepository +): Promise { + const visualizations = await soClient.find({ + type: 'visualization', + perPage: 10000, + }); + + const tableVisualizations = visualizations.saved_objects + .map>(({ attributes }) => JSON.parse(attributes.visState)) + .filter(({ type }) => type === VIS_TYPE_TABLE); + + const defaultStats = { + total: tableVisualizations.length, + total_split: 0, + split_columns: { + total: 0, + enabled: 0, + }, + split_rows: { + total: 0, + enabled: 0, + }, + }; + + return tableVisualizations.reduce((acc, { aggs, params }) => { + const hasSplitAgg = aggs.find((agg) => agg.schema === 'split'); + + if (hasSplitAgg) { + acc.total_split += 1; + + const isSplitRow = params.row; + const isSplitEnabled = hasSplitAgg.enabled; + + const container = isSplitRow ? acc.split_rows : acc.split_columns; + container.total += 1; + container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled; + } + + return acc; + }, defaultStats); +} diff --git a/src/plugins/vis_type_table/server/usage_collector/index.ts b/src/plugins/vis_type_table/server/usage_collector/index.ts new file mode 100644 index 0000000000000..090ed3077b27c --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { registerVisTypeTableUsageCollector } from './register_usage_collector'; diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts new file mode 100644 index 0000000000000..cbf39a4d937a7 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +jest.mock('./get_stats', () => ({ + getStats: jest.fn().mockResolvedValue({ somestat: 1 }), +})); + +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; + +import { registerVisTypeTableUsageCollector } from './register_usage_collector'; +import { getStats } from './get_stats'; + +describe('registerVisTypeTableUsageCollector', () => { + it('Usage collector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'vis_type_table', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('Usage collector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); + expect(getStats).toBeCalledTimes(1); + expect(getStats).toBeCalledWith(mockCollectorFetchContext.soClient); + expect(fetchResult).toEqual({ somestat: 1 }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts new file mode 100644 index 0000000000000..2ac4ce22a47e4 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { getStats, VisTypeTableUsage } from './get_stats'; + +export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) { + const collector = collectorSet.makeUsageCollector({ + type: 'vis_type_table', + isReady: () => true, + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + fetch: ({ soClient }) => getStats(soClient), + }); + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index bda86d06c0ff7..ccff3c349cf21 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -8,6 +8,7 @@ "declarationMap": true }, "include": [ + "common/**/*", "public/**/*", "server/**/*", "*.ts" diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts new file mode 100644 index 0000000000000..d4133eb9b7163 --- /dev/null +++ b/src/plugins/visualizations/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** @public types */ +export * from './types'; diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts new file mode 100644 index 0000000000000..4881b82a0e8d3 --- /dev/null +++ b/src/plugins/visualizations/common/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectAttributes } from 'kibana/server'; +import { AggConfigOptions } from 'src/plugins/data/common'; + +export interface VisParams { + [key: string]: any; +} + +export interface SavedVisState { + title: string; + type: string; + params: TVisParams; + aggs: AggConfigOptions[]; +} + +export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { + description: string; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; + title: string; + version: number; + visState: string; + uiStateJSON: string; +} diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index d1976cc84acec..0bf8aa6e5c418 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -34,7 +34,7 @@ export type { Schema, ISchemas, } from './vis_types'; -export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; +export { SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; @@ -46,12 +46,13 @@ export { PersistedState } from './persisted_state'; export { VisualizationControllerConstructor, VisualizationController, - SavedVisState, ISavedVis, VisSavedObject, VisResponseValue, VisToExpressionAst, + VisParams, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; +export { SavedVisState } from '../common'; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts index f3643ad6adcbe..0d871b3b1dea4 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts +++ b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { SavedVisState } from '../types'; +import { SavedVisState } from '../../common'; declare function updateOldState(oldState: unknown): SavedVisState; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts index c858306968ad8..a85a1d453a939 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts @@ -7,7 +7,8 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject, SavedVisState } from '../types'; +import { VisSavedObject } from '../types'; +import { SavedVisState } from '../../common'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 2e57cd00486f7..dc9ca49840561 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -7,15 +7,12 @@ */ import { SavedObject } from '../../../plugins/saved_objects/public'; -import { - AggConfigOptions, - SearchSourceFields, - TimefilterContract, -} from '../../../plugins/data/public'; +import { SearchSourceFields, TimefilterContract } from '../../../plugins/data/public'; import { ExpressionAstExpression } from '../../expressions/public'; -import { SerializedVis, Vis, VisParams } from './vis'; +import { SerializedVis, Vis } from './vis'; import { ExprVis } from './expressions/vis'; +import { SavedVisState, VisParams } from '../common/types'; export { Vis, SerializedVis, VisParams }; @@ -30,13 +27,6 @@ export type VisualizationControllerConstructor = new ( vis: ExprVis ) => VisualizationController; -export interface SavedVisState { - title: string; - type: string; - params: VisParams; - aggs: AggConfigOptions[]; -} - export interface ISavedVis { id?: string; title: string; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 58bcdb9ea49c6..56a151fb82ed3 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -30,6 +30,7 @@ import { AggConfigOptions, SearchSourceFields, } from '../../../plugins/data/public'; +import { VisParams } from '../common/types'; export interface SerializedVisData { expression?: string; @@ -56,10 +57,6 @@ export interface VisData { savedSearchId?: string; } -export interface VisParams { - [key: string]: any; -} - const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { const searchSource = inputSearchSource.createCopy(); if (savedSearchId) { From 601d9fd018ffc833d41f9a344271839b209ddb3a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 25 Jan 2021 12:33:26 +0100 Subject: [PATCH 21/60] [Uptime] waterfall view reduce opacity for blocking/waiting color (#88611) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../step_detail/waterfall/data_formatting.test.ts | 14 +++++++------- .../step_detail/waterfall/data_formatting.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 5c0b36874004a..3967e0404a76f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -11,7 +11,7 @@ import { WaterfallDataEntry } from '../../waterfall/types'; describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ - blocked: '#b9a888', + blocked: '#dcd4c4', connect: '#da8b45', dns: '#54b399', font: '#aa6556', @@ -173,10 +173,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, @@ -264,10 +264,10 @@ describe('getSeriesAndDomain', () => { }, Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 84.546ms", }, }, @@ -330,10 +330,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 5e59026fd65f8..3cc0497bda8ec 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -197,7 +197,7 @@ const buildTimingPalette = (): TimingColourPalette => { const palette = Object.values(Timings).reduce>((acc, value) => { switch (value) { case Timings.Blocked: - acc[value] = SAFE_PALETTE[6]; + acc[value] = SAFE_PALETTE[16]; break; case Timings.Dns: acc[value] = SAFE_PALETTE[0]; From f15a1e685cc70844b01e3814f5d264fd1ad43586 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 25 Jan 2021 11:36:48 +0000 Subject: [PATCH 22/60] [ML] Adding jobs stats to functions shared in setup contract (#88673) * [ML] Adding jobs stats to functions shared in setup contract * updating types * adding datafeeds Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../providers/anomaly_detectors.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 3719f4d97779c..42f6eb1f8f9c8 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -5,7 +5,12 @@ */ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; +import { + Job, + JobStats, + Datafeed, + DatafeedStats, +} from '../../../common/types/anomaly_detection_jobs'; import { GetGuards } from '../shared_services'; export interface AnomalyDetectorsProvider { @@ -14,6 +19,9 @@ export interface AnomalyDetectorsProvider { savedObjectsClient: SavedObjectsClientContract ): { jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; + jobStats(jobId?: string): Promise<{ count: number; jobs: JobStats[] }>; + datafeeds(datafeedId?: string): Promise<{ count: number; datafeeds: Datafeed[] }>; + datafeedStats(datafeedId?: string): Promise<{ count: number; datafeeds: DatafeedStats[] }>; }; } @@ -36,6 +44,42 @@ export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetect return body; }); }, + async jobStats(jobId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getJobStats<{ + count: number; + jobs: JobStats[]; + }>(jobId !== undefined ? { job_id: jobId } : undefined); + return body; + }); + }, + async datafeeds(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeeds<{ + count: number; + datafeeds: Datafeed[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, + async datafeedStats(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeedStats<{ + count: number; + datafeeds: DatafeedStats[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, }; }, }; From 349fd5fcade9e6c80bbe3be1b13e50fa6efbc24a Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:26:53 +0300 Subject: [PATCH 23/60] Advanced JSON input functional test (#88609) * Advanced JSON input functional test * Replace string check with object property check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/visualize/_inspector.ts | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index 07b5dfb8a769d..9d4623feef74a 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -6,12 +6,15 @@ * Public License, v 1. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { @@ -23,6 +26,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + describe('advanced input JSON', () => { + it('should have "missing" property with value 10', async () => { + log.debug('Add Max Metric on memory field'); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.selectAggregation('Max', 'metrics'); + await PageObjects.visEditor.selectField('memory', 'metrics'); + + log.debug('Add value to advanced JSON input'); + await PageObjects.visEditor.toggleAdvancedParams('2'); + await testSubjects.setValue('codeEditorContainer', '{ "missing": 10 }'); + await PageObjects.visEditor.clickGo(); + + await inspector.open(); + await inspector.openInspectorRequestsView(); + const requestTab = await inspector.getOpenRequestDetailRequestButton(); + await requestTab.click(); + const requestJSON = JSON.parse(await inspector.getCodeEditorValue()); + + expect(requestJSON.aggs['2'].max).property('missing', 10); + }); + + after(async () => { + await inspector.close(); + await PageObjects.visEditor.removeDimension(2); + await PageObjects.visEditor.clickGo(); + }); + }); + describe('inspector table', function indexPatternCreation() { it('should update table header when columns change', async function () { await inspector.open(); From 191ad14ac2ec5e275750b9d93db72c71a1d7ef98 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:47:03 +0300 Subject: [PATCH 24/60] Small multiples in vis_type_xy plugin (#86880) * Small multiples in vis_type_xy plugin * Fix tooltip and formatted split chart values * update advanced settings wording * Remove React import in files with no JSX and change the extension to .ts * Simplify conditions * fix bar interval on split charts in vislib * Fix charts not splitting for terms boolean fields * fix filtering for small multiples * Change tests interval values from 100 to 1000000 * Revert "Change tests interval values from 100 to 1000000" This reverts commit 92f9d1b4b9e0b1f3a6c6d1058c36c657d2c5c68f. * Fix tests for interval issue in vislib (cherry picked from commit ef45b63c47da403399f76f00b49329531d445f31) * Revert axis_scale changes related to interval * Enable _line_chart_split_chart test for new charts library * Move chart splitter id to const Co-authored-by: nickofthyme Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 2 +- .../static/utils/transform_click_event.ts | 62 ++++++++++++++++--- .../public/vislib/lib/axis/axis_scale.js | 10 +-- .../vis_type_xy/public/chart_splitter.tsx | 45 ++++++++++++++ .../public/components/detailed_tooltip.tsx | 34 +++++++--- .../vis_type_xy/public/components/index.ts | 1 - .../public/components/split_chart_warning.tsx | 44 ------------- .../vis_type_xy/public/config/get_aspects.ts | 7 ++- .../vis_type_xy/public/types/config.ts | 2 + .../vis_type_xy/public/utils/accessors.tsx | 15 +++-- .../vis_type_xy/public/vis_component.tsx | 40 ++++++++++-- .../vis_type_xy/public/vis_renderer.tsx | 25 +++----- .../public/vis_types/{area.tsx => area.ts} | 9 --- .../vis_types/{histogram.tsx => histogram.ts} | 9 --- .../{horizontal_bar.tsx => horizontal_bar.ts} | 9 --- .../public/vis_types/{line.tsx => line.ts} | 9 --- .../public/vis_types/split_tooltip.tsx | 20 ------ src/plugins/vis_type_xy/server/plugin.ts | 3 +- .../apps/visualize/_line_chart_split_chart.ts | 28 ++++++--- test/functional/apps/visualize/index.ts | 1 + 20 files changed, 213 insertions(+), 162 deletions(-) create mode 100644 src/plugins/vis_type_xy/public/chart_splitter.tsx delete mode 100644 src/plugins/vis_type_xy/public/components/split_chart_warning.tsx rename src/plugins/vis_type_xy/public/vis_types/{area.tsx => area.ts} (94%) rename src/plugins/vis_type_xy/public/vis_types/{histogram.tsx => histogram.ts} (94%) rename src/plugins/vis_type_xy/public/vis_types/{horizontal_bar.tsx => horizontal_bar.ts} (94%) rename src/plugins/vis_type_xy/public/vis_types/{line.tsx => line.ts} (94%) delete mode 100644 src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7e7c8953fd527..c2306b80734d8 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -458,7 +458,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation. +Enables legacy charts library for area, line and bar charts in visualize. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 20865bea2f897..7c28db333cc83 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -30,6 +30,9 @@ export interface BrushTriggerEvent { type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + /** * returns accessor value from string or function accessor * @param datum @@ -82,6 +85,29 @@ const getAllSplitAccessors = ( value, ]); +/** + * Gets value from small multiple accessors + * + * Only handles single small multiple accessor + */ +function getSplitChartValue({ + smHorizontalAccessorValue, + smVerticalAccessorValue, +}: Pick): + | string + | number + | undefined { + if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smHorizontalAccessorValue; + } + + if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smVerticalAccessorValue; + } + + return; +} + /** * Reduces matching column indexes * @@ -92,7 +118,8 @@ const getAllSplitAccessors = ( const columnReducer = ( xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn ) => ( acc: Array<[index: number, id: string]>, { id }: Datatable['columns'][number], @@ -101,6 +128,7 @@ const columnReducer = ( if ( (xAccessor !== null && validateAccessorId(id, xAccessor)) || (yAccessor !== null && validateAccessorId(id, yAccessor)) || + (splitChartAccessor !== undefined && validateAccessorId(id, splitChartAccessor)) || splitAccessors.some(([accessor]) => validateAccessorId(id, accessor)) ) { acc.push([index, id]); @@ -121,13 +149,18 @@ const rowFindPredicate = ( geometry: GeometryValue | null, xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn, + splitChartValue?: string | number ) => (row: Datatable['rows'][number]): boolean => (geometry === null || (xAccessor !== null && getAccessorValue(row, xAccessor) === geometry.x && yAccessor !== null && - getAccessorValue(row, yAccessor) === geometry.y)) && + getAccessorValue(row, yAccessor) === geometry.y && + (splitChartAccessor === undefined || + (splitChartValue !== undefined && + getAccessorValue(row, splitChartAccessor) === splitChartValue)))) && [...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value); /** @@ -142,19 +175,28 @@ export const getFilterFromChartClickEventFn = ( table: Datatable, xAccessor: Accessor | AccessorFn, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate: boolean = false ) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => { const data: ValueClickContext['data']['data'] = []; points.forEach((point) => { const [geometry, { yAccessor, splitAccessors }] = point; + const splitChartValue = getSplitChartValue(point[1]); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(xAccessor, yAccessor, allSplitAccessors), + columnReducer(xAccessor, yAccessor, allSplitAccessors, splitChartAccessor), [] ); const row = table.rows.findIndex( - rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors) + rowFindPredicate( + geometry, + xAccessor, + yAccessor, + allSplitAccessors, + splitChartAccessor, + splitChartValue + ) ); const newData = columns.map(([column, id]) => ({ table, @@ -179,16 +221,20 @@ export const getFilterFromChartClickEventFn = ( * Helper function to get filter action event from series */ export const getFilterFromSeriesFn = (table: Datatable) => ( - { splitAccessors }: XYChartSeriesIdentifier, + { splitAccessors, ...rest }: XYChartSeriesIdentifier, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate = false ): ClickTriggerEvent => { + const splitChartValue = getSplitChartValue(rest); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(null, null, allSplitAccessors), + columnReducer(null, null, allSplitAccessors, splitChartAccessor), [] ); - const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors)); + const row = table.rows.findIndex( + rowFindPredicate(null, null, null, allSplitAccessors, splitChartAccessor, splitChartValue) + ); const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({ table, column, diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js index 157523cdf09f4..ee9bed141fe4b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js @@ -7,7 +7,7 @@ */ import d3 from 'd3'; -import _ from 'lodash'; +import { isNumber, reduce, times } from 'lodash'; import moment from 'moment'; import { InvalidLogScaleValues } from '../../errors'; @@ -62,7 +62,7 @@ export class AxisScale { return d3[extent]( opts.reduce(function (opts, v) { - if (!_.isNumber(v)) v = +v; + if (!isNumber(v)) v = +v; if (!isNaN(v)) opts.push(v); return opts; }, []) @@ -90,7 +90,7 @@ export class AxisScale { const y = moment(x); const method = n > 0 ? 'add' : 'subtract'; - _.times(Math.abs(n), function () { + times(Math.abs(n), function () { y[method](interval); }); @@ -100,7 +100,7 @@ export class AxisScale { getAllPoints() { const config = this.axisConfig; const data = this.visConfig.data.chartData(); - const chartPoints = _.reduce( + const chartPoints = reduce( data, (chartPoints, chart, chartIndex) => { const points = chart.series.reduce((points, seri, seriIndex) => { @@ -254,6 +254,6 @@ export class AxisScale { } validateScale(scale) { - if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale); + if (!scale || Number.isNaN(scale)) throw new Error('scale is ' + scale); } } diff --git a/src/plugins/vis_type_xy/public/chart_splitter.tsx b/src/plugins/vis_type_xy/public/chart_splitter.tsx new file mode 100644 index 0000000000000..bf63ac1896bd1 --- /dev/null +++ b/src/plugins/vis_type_xy/public/chart_splitter.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts'; + +interface ChartSplitterProps { + splitColumnAccessor?: Accessor | AccessorFn; + splitRowAccessor?: Accessor | AccessorFn; + sort?: GroupBySort; +} + +const CHART_SPLITTER_ID = '__chart_splitter__'; + +export const ChartSplitter = ({ + splitColumnAccessor, + splitRowAccessor, + sort, +}: ChartSplitterProps) => + splitColumnAccessor || splitRowAccessor ? ( + <> + { + const splitTypeAccessor = splitColumnAccessor || splitRowAccessor; + if (splitTypeAccessor) { + return typeof splitTypeAccessor === 'function' + ? splitTypeAccessor(datum) + : datum[splitTypeAccessor]; + } + return spec.id; + }} + sort={sort || 'dataIndex'} + /> + + + ) : null; diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index 49b2ab483bc55..02c7157d32c27 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -16,19 +16,20 @@ import { XYChartSeriesIdentifier, } from '@elastic/charts'; -import { BUCKET_TYPES } from '../../../data/public'; - import { Aspects } from '../types'; import './_detailed_tooltip.scss'; import { fillEmptyValue } from '../utils/get_series_name_fn'; -import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors'; +import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors'; interface TooltipData { label: string; value: string; } +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + const getTooltipData = ( aspects: Aspects, header: TooltipValue | null, @@ -37,10 +38,7 @@ const getTooltipData = ( const data: TooltipData[] = []; if (header) { - const xFormatter = - aspects.x.aggType === BUCKET_TYPES.DATE_RANGE || aspects.x.aggType === BUCKET_TYPES.RANGE - ? null - : aspects.x.formatter; + const xFormatter = isRangeAggType(aspects.x.aggType) ? null : aspects.x.formatter; data.push({ label: aspects.x.title, value: xFormatter ? xFormatter(header.value) : `${header.value}`, @@ -80,6 +78,28 @@ const getTooltipData = ( } }); + if ( + aspects.splitColumn && + valueSeries.smHorizontalAccessorValue !== undefined && + valueSeries.smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitColumn.title, + value: `${valueSeries.smHorizontalAccessorValue}`, + }); + } + + if ( + aspects.splitRow && + valueSeries.smVerticalAccessorValue !== undefined && + valueSeries.smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitRow.title, + value: `${valueSeries.smVerticalAccessorValue}`, + }); + } + return data; }; diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts index 260c08e0fc4a9..9b2559bafd18e 100644 --- a/src/plugins/vis_type_xy/public/components/index.ts +++ b/src/plugins/vis_type_xy/public/components/index.ts @@ -11,4 +11,3 @@ export { XYEndzones } from './xy_endzones'; export { XYCurrentTime } from './xy_current_time'; export { XYSettings } from './xy_settings'; export { XYThresholdLine } from './xy_threshold_line'; -export { SplitChartWarning } from './split_chart_warning'; diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx deleted file mode 100644 index b708590e04479..0000000000000 --- a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx +++ /dev/null @@ -1,44 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React, { FC } from 'react'; - -import { EuiLink, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { getDocLinks } from '../services'; - -export const SplitChartWarning: FC = () => { - const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; - - return ( - - - - - ), - }} - /> - - ); -}; diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts index b8da4386806d4..c031d3fa1fb9b 100644 --- a/src/plugins/vis_type_xy/public/config/get_aspects.ts +++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts @@ -29,7 +29,10 @@ export function getEmptyAspect(): Aspect { }, }; } -export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dimensions): Aspects { +export function getAspects( + columns: DatatableColumn[], + { x, y, z, series, splitColumn, splitRow }: Dimensions +): Aspects { const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series]; return { @@ -37,6 +40,8 @@ export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dime y: getAspectsFromDimension(columns, y) ?? [], z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined, series: getAspectsFromDimension(columns, seriesDimensions), + splitColumn: splitColumn?.length ? getAspectsFromDimension(columns, splitColumn[0]) : undefined, + splitRow: splitRow?.length ? getAspectsFromDimension(columns, splitRow[0]) : undefined, }; } diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index af3d840739f17..9d4660afa1634 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -43,6 +43,8 @@ export interface Aspects { y: Aspect[]; z?: Aspect; series?: Aspect[]; + splitColumn?: Aspect; + splitRow?: Aspect; } export interface AxisGrid { diff --git a/src/plugins/vis_type_xy/public/utils/accessors.tsx b/src/plugins/vis_type_xy/public/utils/accessors.tsx index d1337251d36aa..e40248ae92e12 100644 --- a/src/plugins/vis_type_xy/public/utils/accessors.tsx +++ b/src/plugins/vis_type_xy/public/utils/accessors.tsx @@ -26,11 +26,15 @@ const getFieldName = (fieldName: string, index?: number) => { return `${fieldName}${indexStr}`; }; +export const isRangeAggType = (type: string | null) => + type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE; + /** * Returns accessor function for complex accessor types * @param aspect + * @param isComplex - forces to be functional/complex accessor */ -export const getComplexAccessor = (fieldName: string) => ( +export const getComplexAccessor = (fieldName: string, isComplex: boolean = false) => ( aspect: Aspect, index?: number ): Accessor | AccessorFn | undefined => { @@ -38,12 +42,7 @@ export const getComplexAccessor = (fieldName: string) => ( return; } - if ( - !( - (aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) && - aspect.formatter - ) - ) { + if (!((isComplex || isRangeAggType(aspect.aggType)) && aspect.formatter)) { return aspect.accessor; } @@ -51,7 +50,7 @@ export const getComplexAccessor = (fieldName: string) => ( const accessor = aspect.accessor; const fn: AccessorFn = (d) => { const v = d[accessor]; - if (!v) { + if (v === undefined) { return; } const f = formatter(v); diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 6f994707cbb72..871fb408d4da0 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -65,6 +65,7 @@ import { getComplexAccessor, getSplitSeriesAccessorFnMap, } from './utils/accessors'; +import { ChartSplitter } from './chart_splitter'; export interface VisComponentProps { visParams: VisParams; @@ -117,7 +118,8 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ): ElementClickListener => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (elements) => { @@ -125,7 +127,8 @@ const VisComponent = (props: VisComponentProps) => { const event = getFilterFromChartClickEventFn( visData, xAccessor, - splitSeriesAccessorFnMap + splitSeriesAccessorFnMap, + splitChartAccessor )(elements as XYChartElementEvent[]); props.fireEvent(event); } @@ -154,12 +157,17 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ) => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (series: XYChartSeriesIdentifier): ClickTriggerEvent | null => { if (xAccessor !== null) { - return getFilterFromSeriesFn(visData)(series, splitSeriesAccessorFnMap); + return getFilterFromSeriesFn(visData)( + series, + splitSeriesAccessorFnMap, + splitChartAccessor + ); } return null; @@ -304,6 +312,12 @@ const VisComponent = (props: VisComponentProps) => { : [], [config.aspects.series] ); + const splitChartColumnAccessor = config.aspects.splitColumn + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitColumn) + : undefined; + const splitChartRowAccessor = config.aspects.splitRow + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitRow) + : undefined; const renderSeries = useMemo( () => @@ -336,6 +350,10 @@ const VisComponent = (props: VisComponentProps) => { legendPosition={legendPosition} /> + { xDomain={xDomain} adjustedXDomain={adjustedXDomain} legendColorPicker={useColorPicker(legendPosition, setColor, getSeriesName)} - onElementClick={handleFilterClick(visData, xAccessor, splitSeriesAccessors)} + onElementClick={handleFilterClick( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + )} onBrushEnd={handleBrush(visData, xAccessor, 'interval' in config.aspects.x.params)} onRenderChange={onRenderChange} legendAction={ config.aspects.series && (config.aspects.series?.length ?? 0) > 0 ? getLegendActions( canFilter, - getFilterEventData(visData, xAccessor, splitSeriesAccessors), + getFilterEventData( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + ), handleFilterAction, getSeriesName ) diff --git a/src/plugins/vis_type_xy/public/vis_renderer.tsx b/src/plugins/vis_type_xy/public/vis_renderer.tsx index 612388939d26b..1a47742b3d004 100644 --- a/src/plugins/vis_type_xy/public/vis_renderer.tsx +++ b/src/plugins/vis_type_xy/public/vis_renderer.tsx @@ -16,7 +16,6 @@ import { VisualizationContainer } from '../../visualizations/public'; import type { PersistedState } from '../../visualizations/public'; import { XyVisType } from '../common'; -import { SplitChartWarning } from './components/split_chart_warning'; import { VisComponentType } from './vis_component'; import { RenderValue, visName } from './xy_vis_fn'; @@ -36,24 +35,20 @@ export const xyVisRenderer: ExpressionRenderDefinition = { reuseDomNode: true, render: async (domNode, { visData, visConfig, visType, syncColors }, handlers) => { const showNoResult = shouldShowNoResultsMessage(visData, visType); - const isSplitChart = Boolean(visConfig.dimensions.splitRow); handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( - <> - {isSplitChart && } - - - - + + + , domNode ); diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/area.tsx rename to src/plugins/vis_type_xy/public/vis_types/area.ts index 50721c349d6e9..09007a01ca8bc 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getAreaVisTypeDefinition = ( showElasticChartsOptions = false @@ -181,12 +178,6 @@ export const getAreaVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/histogram.tsx rename to src/plugins/vis_type_xy/public/vis_types/histogram.ts index 4fc8dbbb80e7b..daae5f5e48e61 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHistogramVisTypeDefinition = ( showElasticChartsOptions = false @@ -184,12 +181,6 @@ export const getHistogramVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx rename to src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index b53bb7bc9dd40..9e026fa0d7474 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHorizontalBarVisTypeDefinition = ( showElasticChartsOptions = false @@ -183,12 +180,6 @@ export const getHorizontalBarVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/line.tsx rename to src/plugins/vis_type_xy/public/vis_types/line.ts index e9b0533b957f5..3f3087207fa19 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getLineVisTypeDefinition = ( showElasticChartsOptions = false @@ -175,12 +172,6 @@ export const getLineVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx b/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx deleted file mode 100644 index ca22136599341..0000000000000 --- a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx +++ /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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export function SplitTooltip() { - return ( - - ); -} diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index bd7957164fd1a..fa3dddfeca02a 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -24,8 +24,7 @@ export const uiSettingsConfig: Record> = { description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', { - defaultMessage: - 'Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation.', + defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', } ), category: ['visualization'], diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index aeb80a58c9655..3e74bf0b7c0ec 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -176,8 +176,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -188,8 +188,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -201,7 +201,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -210,7 +213,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -220,7 +226,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -228,7 +237,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index dddcd82f1d3f8..8dd2854419693 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // Test replaced vislib chart types loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_line_chart_split_series')); + loadTestFile(require.resolve('./_line_chart_split_chart')); loadTestFile(require.resolve('./_point_series_options')); loadTestFile(require.resolve('./_vertical_bar_chart')); loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); From 16657028f1b188d959a1384a82e4557f0a168e09 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 25 Jan 2021 16:16:25 +0300 Subject: [PATCH 25/60] [Saved Objects] Fix saved object view path (#89057) * Fix saved object view path * Add additional check --- .../public/lib/create_field_list.ts | 3 ++- .../object_view/components/form.tsx | 5 ++-- .../object_view/saved_object_view.tsx | 18 +++++++------ .../saved_objects_edition_page.tsx | 26 +++++++++++-------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/plugins/saved_objects_management/public/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts index cd30a02bd0ef3..4497fb04ffa2c 100644 --- a/src/plugins/saved_objects_management/public/lib/create_field_list.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts @@ -11,11 +11,12 @@ import { SimpleSavedObject } from '../../../../core/public'; import { castEsToKbnFieldTypeName } from '../../../data/public'; import { ObjectField } from '../management_section/types'; import { SavedObjectLoader } from '../../../saved_objects/public'; +import { SavedObjectWithMetadata } from '../types'; const maxRecursiveIterations = 20; export function createFieldList( - object: SimpleSavedObject, + object: SimpleSavedObject | SavedObjectWithMetadata, service?: SavedObjectLoader ): ObjectField[] { let fields = Object.entries(object.attributes as Record).reduce( diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index 96a4a24f6591e..e048b92b9566c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -19,14 +19,15 @@ import { set } from '@elastic/safer-lodash-set'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; +import { SavedObjectsClientContract } from '../../../../../../core/public'; import { SavedObjectLoader } from '../../../../../saved_objects/public'; import { Field } from './field'; import { ObjectField, FieldState, SubmittedFormData } from '../../types'; import { createFieldList } from '../../../lib'; +import { SavedObjectWithMetadata } from '../../../types'; interface FormProps { - object: SimpleSavedObject; + object: SavedObjectWithMetadata; service: SavedObjectLoader; savedObjectsClient: SavedObjectsClientContract; editionEnabled: boolean; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 31c0a76e16f58..3343e0a63f54c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -14,16 +14,18 @@ import { SavedObjectsClientContract, OverlayStart, NotificationsStart, - SimpleSavedObject, ScopedHistory, + HttpSetup, } from '../../../../../core/public'; import { ISavedObjectsManagementServiceRegistry } from '../../services'; import { Header, NotFoundErrors, Intro, Form } from './components'; -import { canViewInApp } from '../../lib'; +import { canViewInApp, findObject } from '../../lib'; import { SubmittedFormData } from '../types'; +import { SavedObjectWithMetadata } from '../../types'; interface SavedObjectEditionProps { id: string; + http: HttpSetup; serviceName: string; serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; @@ -36,7 +38,7 @@ interface SavedObjectEditionProps { interface SavedObjectEditionState { type: string; - object?: SimpleSavedObject; + object?: SavedObjectWithMetadata; } export class SavedObjectEdition extends Component< @@ -56,9 +58,9 @@ export class SavedObjectEdition extends Component< } componentDidMount() { - const { id, savedObjectsClient } = this.props; + const { http, id } = this.props; const { type } = this.state; - savedObjectsClient.get(type, id).then((object) => { + findObject(http, type, id).then((object) => { this.setState({ object, }); @@ -70,7 +72,7 @@ export class SavedObjectEdition extends Component< capabilities, notFoundType, serviceRegistry, - id, + http, serviceName, savedObjectsClient, } = this.props; @@ -80,7 +82,7 @@ export class SavedObjectEdition extends Component< string, boolean >; - const canView = canViewInApp(capabilities, type); + const canView = canViewInApp(capabilities, type) && Boolean(object?.meta.inAppUrl?.path); const service = serviceRegistry.get(serviceName)!.service; return ( @@ -91,7 +93,7 @@ export class SavedObjectEdition extends Component< canViewInApp={canView} type={type} onDeleteClick={() => this.delete()} - viewUrl={service.urlFor(id)} + viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')} /> {notFoundType && ( <> diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx index 758789aa0f47e..2af7c22488c51 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -11,6 +11,7 @@ import { useParams, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; import { CoreStart, ChromeBreadcrumb, ScopedHistory } from 'src/core/public'; +import { RedirectAppLinks } from '../../../kibana_react/public'; import { ISavedObjectsManagementServiceRegistry } from '../services'; import { SavedObjectEdition } from './object_view'; @@ -50,17 +51,20 @@ const SavedObjectsEditionPage = ({ }, [setBreadcrumbs, service]); return ( - + + + ); }; From 50d8c69ea800f778c8530a1c56036ea590c7569d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Jan 2021 13:36:38 +0000 Subject: [PATCH 26/60] skip flaky suite (#88639) --- .../functional/tests/visualize_integration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index e92ba226f3959..51f4bf8883521 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -151,7 +151,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('editing', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88639 + describe.skip('editing', () => { beforeEach(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.waitUntilTableIsLoaded(); From 99ffbbe357b5f80f10925f71e7d5862403f0a009 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 25 Jan 2021 14:46:23 +0100 Subject: [PATCH 27/60] [Lens] Restore a11y flacky tests (#88954) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/accessibility/apps/lens.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index a7cacd0ad1cbb..71673d49c0c08 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -7,15 +7,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'home', 'settings', 'lens']); + const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'home', 'lens']); const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/88926 - // FLAKY: https://github.com/elastic/kibana/issues/88927 - // FLAKY: https://github.com/elastic/kibana/issues/88929 - describe.skip('Lens', () => { + describe('Lens', () => { const lensChartName = 'MyLensChart'; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -35,12 +32,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('lens', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await a11y.testAppSnapshot(); }); it('lens XY chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -75,6 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dimension configuration panel', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel > lns-empty-dimension'); await a11y.testAppSnapshot(); From a5bb86482d346c905d2946e667055cc0889d320a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Jan 2021 13:49:20 +0000 Subject: [PATCH 28/60] skip flaky suite (#89069) --- .../apps/management/search_sessions/sessions_management.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f06e8eba0bf68..e3797550984aa 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - describe('Search search sessions Management UI', () => { + // FLAKY: https://github.com/elastic/kibana/issues/89069 + describe.skip('Search search sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); From aeb6df30d5b987579fcef03805a17e3c5b7da224 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Mon, 25 Jan 2021 13:51:57 +0000 Subject: [PATCH 29/60] Update user management page (#87133) * Update user management page * Fixed i18n errors * Fix linting errors * Add ids required for accessability * Added suggestions from code review * Fix test errors * Fix types in fleet * fix translations * Fix i18n * Added suggestions from code review * Fix i18n errors * Fix linting errors * Update messaging * Updated unit tests * Updated functional tests * Fixed functional tests * Fix linting errors * Fix React warnings * Added suggestions from code review * Added tests and renamed routes * Fix functional tests * Simplified API integration tests * Updated copy based on writing suggestions * Fixed unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../components/agent_logs/agent_logs.tsx | 2 +- x-pack/plugins/security/common/constants.ts | 13 + x-pack/plugins/security/common/model/index.ts | 2 + x-pack/plugins/security/common/model/role.ts | 22 +- .../security/public/components/breadcrumb.tsx | 137 +++ .../public/components/confirm_modal.tsx | 94 ++ .../security/public/components/doc_link.tsx | 60 ++ .../public/components/form_flyout.tsx | 97 ++ .../public/components/use_current_user.ts | 24 + .../security/public/components/use_form.ts | 205 +++++ .../security/public/components/use_html_id.ts | 27 + .../role_combo_box/role_combo_box.test.tsx | 266 +++--- .../role_combo_box/role_combo_box.tsx | 128 ++- .../role_combo_box_option.test.tsx | 57 -- .../role_combo_box/role_combo_box_option.tsx | 31 - .../roles/edit_role/edit_role_page.tsx | 2 +- .../roles/edit_role/validate_role.test.ts | 28 +- .../roles/edit_role/validate_role.ts | 22 +- .../edit_user/change_password_flyout.tsx | 286 ++++++ .../users/edit_user/confirm_delete_users.tsx | 96 ++ .../users/edit_user/confirm_disable_users.tsx | 119 +++ .../users/edit_user/confirm_enable_users.tsx | 89 ++ .../users/edit_user/create_user_page.test.tsx | 108 +++ .../users/edit_user/create_user_page.tsx | 44 + .../users/edit_user/edit_user_page.scss | 6 - .../users/edit_user/edit_user_page.test.tsx | 566 ++++++++---- .../users/edit_user/edit_user_page.tsx | 836 ++++++------------ .../management/users/edit_user/index.ts | 1 + .../management/users/edit_user/user_form.tsx | 466 ++++++++++ .../users/edit_user/validate_user.test.ts | 128 --- .../users/edit_user/validate_user.ts | 142 --- .../management/users/user_api_client.mock.ts | 2 + .../management/users/user_api_client.ts | 10 +- .../users/users_grid/users_grid_page.tsx | 2 +- .../users/users_management_app.test.tsx | 129 +-- .../management/users/users_management_app.tsx | 158 ++-- .../server/routes/users/create_or_update.ts | 7 +- .../security/server/routes/users/disable.ts | 32 + .../security/server/routes/users/enable.ts | 32 + .../security/server/routes/users/index.ts | 4 + .../translations/translations/ja-JP.json | 34 +- .../translations/translations/zh-CN.json | 34 +- x-pack/test/accessibility/apps/users.ts | 47 +- .../api_integration/apis/security/index.ts | 1 + .../apis/security/security_basic.ts | 1 + .../api_integration/apis/security/users.ts | 47 + .../dashboard_mode/dashboard_view_mode.js | 58 +- .../apps/security/doc_level_security_roles.js | 8 +- .../apps/security/field_level_security.js | 16 +- .../functional/apps/security/rbac_phase1.js | 14 +- .../functional/apps/security/role_mappings.ts | 5 +- .../apps/security/secure_roles_perm.js | 8 +- .../functional/apps/security/user_email.js | 7 +- x-pack/test/functional/apps/security/users.js | 12 +- .../functional/page_objects/security_page.ts | 108 ++- yarn.lock | 40 +- 57 files changed, 3242 insertions(+), 1680 deletions(-) create mode 100644 x-pack/plugins/security/public/components/breadcrumb.tsx create mode 100644 x-pack/plugins/security/public/components/confirm_modal.tsx create mode 100644 x-pack/plugins/security/public/components/doc_link.tsx create mode 100644 x-pack/plugins/security/public/components/form_flyout.tsx create mode 100644 x-pack/plugins/security/public/components/use_current_user.ts create mode 100644 x-pack/plugins/security/public/components/use_form.ts create mode 100644 x-pack/plugins/security/public/components/use_html_id.ts delete mode 100644 x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx delete mode 100644 x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx create mode 100644 x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx delete mode 100644 x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss create mode 100644 x-pack/plugins/security/public/management/users/edit_user/user_form.tsx delete mode 100644 x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts delete mode 100644 x-pack/plugins/security/public/management/users/edit_user/validate_user.ts create mode 100644 x-pack/plugins/security/server/routes/users/disable.ts create mode 100644 x-pack/plugins/security/server/routes/users/enable.ts create mode 100644 x-pack/test/api_integration/apis/security/users.ts diff --git a/package.json b/package.json index 2fdc31820b9d4..24297011ccc63 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,7 @@ "react-resizable": "^1.7.5", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-use": "^13.27.0", + "react-use": "^15.3.4", "recompose": "^0.26.0", "redux": "^4.0.5", "redux-actions": "^2.6.5", diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 7326d2efb8565..8a4cf1d8566a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -183,7 +183,7 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); + const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index f53b5ca6d56ca..c235c296bcbae 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -22,3 +22,16 @@ export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider'; export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg'; export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; + +/** + * Matches valid usernames and role names. + * + * - Must contain only letters, numbers, spaces, punctuation and printable symbols. + * - Must not contain leading or trailing spaces. + */ +export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(?) { * @param {role} the Role as returned by roles API */ export function isRoleDeprecated(role: Partial) { - return role.metadata?._deprecated ?? false; + return (role.metadata?._deprecated as boolean) ?? false; +} + +/** + * Returns whether given role is a system role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleSystem(role: Partial) { + return (isRoleReserved(role) && role.name?.endsWith('_system')) ?? false; +} + +/** + * Returns whether given role is an admin role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleAdmin(role: Partial) { + return ( + (isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false + ); } /** diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx new file mode 100644 index 0000000000000..7246e37b33da9 --- /dev/null +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useEffect, useRef, useContext, FunctionComponent } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; + +interface BreadcrumbsContext { + parents: BreadcrumbProps[]; + onMount(breadcrumbs: BreadcrumbProps[]): void; + onUnmount(breadcrumbs: BreadcrumbProps[]): void; +} + +const BreadcrumbsContext = createContext(undefined); + +export interface BreadcrumbProps extends EuiBreadcrumb { + text: string; +} + +/** + * Component that automatically sets breadcrumbs and document title based on the render tree. + * + * @example + * // Breadcrumbs will be set to: "Users > Create" + * // Document title will be set to: "Create - Users" + * + * ```typescript + * + * + * {showForm && ( + * + *
+ *
+ * )} + * + * ``` + */ +export const Breadcrumb: FunctionComponent = ({ children, ...breadcrumb }) => { + const context = useContext(BreadcrumbsContext); + const component = {children}; + + if (context) { + return component; + } + + return {component}; +}; + +export interface BreadcrumbsProviderProps { + onChange?: BreadcrumbsChangeHandler; +} + +export type BreadcrumbsChangeHandler = (breadcrumbs: BreadcrumbProps[]) => void; + +/** + * Component that can be used to define any side effects that should occur when breadcrumbs change. + * + * By default the breadcrumbs in application chrome are set and the document title is updated. + * + * @example + * ```typescript + * setBreadcrumbs(breadcrumbs)}> + * + * + * ``` + */ +export const BreadcrumbsProvider: FunctionComponent = ({ + children, + onChange, +}) => { + const { services } = useKibana(); + const breadcrumbsRef = useRef([]); + + const handleChange = (breadcrumbs: BreadcrumbProps[]) => { + if (onChange) { + onChange(breadcrumbs); + } else if (services.chrome) { + services.chrome.setBreadcrumbs(breadcrumbs); + services.chrome.docTitle.change(getDocTitle(breadcrumbs)); + } + }; + + return ( + { + if (breadcrumbs.length > breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + onUnmount: (breadcrumbs) => { + if (breadcrumbs.length < breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + }} + > + {children} + + ); +}; + +export interface InnerBreadcrumbProps { + breadcrumb: BreadcrumbProps; +} + +export const InnerBreadcrumb: FunctionComponent = ({ + breadcrumb, + children, +}) => { + const { parents, onMount, onUnmount } = useContext(BreadcrumbsContext)!; + const nextParents = [...parents, breadcrumb]; + + useEffect(() => { + onMount(nextParents); + return () => onUnmount(parents); + }, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {children} + + ); +}; + +export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) { + return breadcrumbs + .slice(0, maxBreadcrumbs) + .reverse() + .map(({ text }) => text); +} diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx new file mode 100644 index 0000000000000..8dfbf9e3f0649 --- /dev/null +++ b/x-pack/plugins/security/public/components/confirm_modal.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalProps, + EuiOverlayMask, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ConfirmModalProps extends Omit { + confirmButtonText: string; + confirmButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; + onCancel(): void; + onConfirm(): void; + ownFocus?: boolean; +} + +/** + * Component that renders a confirmation modal similar to `EuiConfirmModal`, except that + * it adds `isLoading` prop, which renders a loading spinner and disables action buttons, + * and `ownFocus` prop to render overlay mask. + */ +export const ConfirmModal: FunctionComponent = ({ + children, + confirmButtonColor: buttonColor, + confirmButtonText, + isLoading, + isDisabled, + onCancel, + onConfirm, + ownFocus = true, + title, + ...rest +}) => { + const modal = ( + + + {title} + + {children} + + + + + + + + + + {confirmButtonText} + + + + + + ); + + return ownFocus ? ( + {modal} + ) : ( + modal + ); +}; diff --git a/x-pack/plugins/security/public/components/doc_link.tsx b/x-pack/plugins/security/public/components/doc_link.tsx new file mode 100644 index 0000000000000..50a93b8ee5090 --- /dev/null +++ b/x-pack/plugins/security/public/components/doc_link.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, FunctionComponent } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { CoreStart } from '../../../../../src/core/public'; + +export type DocLinks = CoreStart['docLinks']['links']; +export type GetDocLinkFunction = (app: string, doc: string) => string; + +/** + * Creates links to the documentation. + * + * @see {@link DocLink} for a component that creates a link to the docs. + * + * @example + * ```typescript + * + * Learn what privileges individual roles grant. + * + * ``` + * + * @example + * ```typescript + * const [docs] = useDocLinks(); + * + * + * Learn how to get started with dashboards. + * + * ``` + */ +export function useDocLinks(): [DocLinks, GetDocLinkFunction] { + const { services } = useKibana(); + const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!; + const getDocLink = useCallback( + (app, doc) => { + return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`; + }, + [ELASTIC_WEBSITE_URL, DOC_LINK_VERSION] + ); + return [links, getDocLink]; +} + +export interface DocLinkProps { + app: string; + doc: string; +} + +export const DocLink: FunctionComponent = ({ app, doc, children }) => { + const [, getDocLink] = useDocLinks(); + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx new file mode 100644 index 0000000000000..a0d397f81751e --- /dev/null +++ b/x-pack/plugins/security/public/components/form_flyout.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, FunctionComponent, RefObject } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiFlyout, + EuiFlyoutProps, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiPortal, +} from '@elastic/eui'; +import { useHtmlId } from './use_html_id'; + +export interface FormFlyoutProps extends Omit { + title: string; + initialFocus?: RefObject; + onCancel(): void; + onSubmit(): void; + submitButtonText: string; + submitButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; +} + +export const FormFlyout: FunctionComponent = ({ + title, + submitButtonText, + submitButtonColor, + onCancel, + onSubmit, + isLoading, + isDisabled, + children, + initialFocus, + ...rest +}) => { + useEffect(() => { + if (initialFocus && initialFocus.current) { + initialFocus.current.focus(); + } + }, [initialFocus]); + + const titleId = useHtmlId('formFlyout', 'title'); + + return ( + + + + +

{title}

+
+
+ {children} + + + + + + + + + + {submitButtonText} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/components/use_current_user.ts b/x-pack/plugins/security/public/components/use_current_user.ts new file mode 100644 index 0000000000000..b686e0ae9d778 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_current_user.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import useAsync from 'react-use/lib/useAsync'; +import constate from 'constate'; +import { AuthenticationServiceSetup } from '../authentication'; + +export interface AuthenticationProviderProps { + authc: AuthenticationServiceSetup; +} + +const [AuthenticationProvider, useAuthentication] = constate( + ({ authc }: AuthenticationProviderProps) => authc +); + +export { AuthenticationProvider, useAuthentication }; + +export function useCurrentUser() { + const authc = useAuthentication(); + return useAsync(authc.getCurrentUser, [authc]); +} diff --git a/x-pack/plugins/security/public/components/use_form.ts b/x-pack/plugins/security/public/components/use_form.ts new file mode 100644 index 0000000000000..33c7e184ec171 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_form.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChangeEventHandler, FocusEventHandler, ReactEventHandler, useState } from 'react'; +import { get, set, cloneDeep, cloneDeepWith } from 'lodash'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +export type FormReturnTuple = [FormState, FormProps]; + +export interface FormProps { + onSubmit: ReactEventHandler; + onChange: ChangeEventHandler; + onBlur: FocusEventHandler; +} + +export interface FormOptions { + onSubmit: SubmitCallback; + validate: ValidateCallback; + defaultValues: Values; +} + +/** + * Returns state and {@link HTMLFormElement} event handlers useful for creating + * forms with inline validation. + * + * @see {@link useFormState} if you don't want to use {@link HTMLFormElement}. + * + * @example + * ```typescript + * const [form, eventHandlers] = useForm({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.email ? { email: 'Required' } : {} + * }); + * + * + * + * Submit + * + * ``` + */ +export function useForm( + options: FormOptions +): FormReturnTuple { + const form = useFormState(options); + + const eventHandlers: FormProps = { + onSubmit: (event) => { + event.preventDefault(); + form.submit(); + }, + onChange: (event) => { + const { name, type, checked, value } = event.target; + if (name) { + form.setValue(name, type === 'checkbox' ? checked : value); + } + }, + onBlur: (event) => { + const { name } = event.target; + if (name) { + form.setTouched(event.target.name); + } + }, + }; + + return [form, eventHandlers]; +} + +export type FormValues = Record; +export type SubmitCallback = (values: Values) => Promise; +export type ValidateCallback = ( + values: Values +) => ValidationErrors | Promise>; +export type ValidationErrors = DeepMap; +export type TouchedFields = DeepMap; + +export interface FormState { + setValue(name: string, value: any): Promise; + setError(name: string, message: string): void; + setTouched(name: string): Promise; + reset(values: Values): void; + submit(): Promise; + values: Values; + errors: ValidationErrors; + touched: TouchedFields; + isValidating: boolean; + isSubmitting: boolean; + submitError: Error | undefined; + isInvalid: boolean; + isSubmitted: boolean; +} + +/** + * Returns state useful for creating forms with inline validation. + * + * @example + * ```typescript + * const form = useFormState({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.toggle ? { toggle: 'Required' } : {} + * }); + * + * form.setValue('toggle', e.target.checked)} + * onBlur={() => form.setTouched('toggle')} + * isInvalid={!!form.errors.toggle} + * /> + * + * Submit + * + * ``` + */ +export function useFormState({ + onSubmit, + validate, + defaultValues, +}: FormOptions): FormState { + const [values, setValues] = useState(defaultValues); + const [errors, setErrors] = useState>({}); + const [touched, setTouched] = useState>({}); + const [submitCount, setSubmitCount] = useState(0); + + const [validationState, validateForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validate(formValues); + setErrors(nextErrors); + if (Object.keys(nextErrors).length === 0) { + setSubmitCount(0); + } + return nextErrors; + }, + [validate] + ); + + const [submitState, submitForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validateForm(formValues); + setTouched(mapDeep(formValues, true)); + setSubmitCount(submitCount + 1); + if (Object.keys(nextErrors).length === 0) { + return onSubmit(formValues); + } + }, + [validateForm, onSubmit] + ); + + return { + setValue: async (name, value) => { + const nextValues = setDeep(values, name, value); + setValues(nextValues); + await validateForm(nextValues); + }, + setTouched: async (name, value = true) => { + setTouched(setDeep(touched, name, value)); + await validateForm(values); + }, + setError: (name, message) => { + setErrors(setDeep(errors, name, message)); + setTouched(setDeep(touched, name, true)); + }, + reset: (nextValues) => { + setValues(nextValues); + setErrors({}); + setTouched({}); + setSubmitCount(0); + }, + submit: () => submitForm(values), + values, + errors, + touched, + isValidating: validationState.loading, + isSubmitting: submitState.loading, + submitError: submitState.error, + isInvalid: Object.keys(errors).length > 0, + isSubmitted: submitCount > 0, + }; +} + +type DeepMap = { + [K in keyof T]?: T[K] extends any[] + ? T[K][number] extends object + ? Array> + : TValue + : T[K] extends object + ? DeepMap + : TValue; +}; + +function mapDeep(values: T, value: V): DeepMap { + return cloneDeepWith(values, (v) => { + if (typeof v !== 'object' && v !== null) { + return value; + } + }); +} + +function setDeep(values: T, name: string, value: V): T { + if (get(values, name) !== value) { + return set(cloneDeep(values), name, value); + } + return values; +} diff --git a/x-pack/plugins/security/public/components/use_html_id.ts b/x-pack/plugins/security/public/components/use_html_id.ts new file mode 100644 index 0000000000000..23666e83cbf23 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_html_id.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { htmlIdGenerator } from '@elastic/eui'; + +/** + * Generates an ID that can be used for HTML elements. + * + * @param prefix Prefix of the id to be generated + * @param suffix Suffix of the id to be generated + * + * @example + * ```typescript + * const titleId = useHtmlId('changePasswordForm', 'title'); + * + * + *

Change password

+ *
+ * ``` + */ +export function useHtmlId(prefix?: string, suffix?: string) { + return useMemo(() => htmlIdGenerator(prefix)(suffix), [prefix, suffix]); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx index c5582d3526242..b7808ffb30e74 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx @@ -5,141 +5,153 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; - +import { shallowWithIntl } from '@kbn/test/jest'; import { RoleComboBox } from '.'; -import { EuiComboBox } from '@elastic/eui'; -import { findTestSubject } from '@kbn/test/jest'; describe('RoleComboBox', () => { - it('renders the provided list of roles via EuiComboBox options', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "default", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": false, + it('renders roles grouped by custom, user, admin, system and deprecated roles with correct color', () => { + const wrapper = shallowWithIntl( + { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: { _deprecated: true }, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": true, + { + name: 'some_admin', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, }, - }, - ] - `); - }); - - it('renders the selected role names in the expanded list, coded according to deprecated status', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( -
- -
+ { + name: 'some_system', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, + }, + { + name: 'deprecated_role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: true }, + }, + ]} + selectedRoleNames={[]} + onChange={jest.fn()} + /> ); - findTestSubject(wrapper, 'comboBoxToggleListButton').simulate('click'); - - wrapper.find(EuiComboBox).setState({ isListOpen: true }); - - expect(findTestSubject(wrapper, 'rolesDropdown-renderOption')).toMatchInlineSnapshot(` - Array [ -
- -
- role-1 - -
-
-
, -
- -
- role-2 - -
-
-
, - ] + expect(wrapper).toMatchInlineSnapshot(` + `); }); }); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx index 5b24b296b299f..91d953c4aa29a 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx @@ -6,11 +6,28 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox } from '@elastic/eui'; -import { Role, isRoleDeprecated } from '../../../common/model'; -import { RoleComboBoxOption } from './role_combo_box_option'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiBadge, + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { + Role, + isRoleSystem, + isRoleAdmin, + isRoleReserved, + isRoleDeprecated, +} from '../../../common/model'; -interface Props { +interface Props + extends Omit< + EuiComboBoxProps, + 'onChange' | 'options' | 'selectedOptions' | 'renderOption' + > { availableRoles: Role[]; selectedRoleNames: readonly string[]; onChange: (selectedRoleNames: string[]) => void; @@ -19,43 +36,132 @@ interface Props { isDisabled?: boolean; } +type Option = EuiComboBoxOptionOption<{ + isReserved: boolean; + isDeprecated: boolean; + isSystem: boolean; + isAdmin: boolean; + deprecatedReason?: string; +}>; + export const RoleComboBox = (props: Props) => { const onRolesChange = (selectedItems: Array<{ label: string }>) => { props.onChange(selectedItems.map((item) => item.label)); }; - const roleNameToOption = (roleName: string) => { + const roleNameToOption = (roleName: string): Option => { const roleDefinition = props.availableRoles.find((role) => role.name === roleName); + const isReserved: boolean = (roleDefinition && isRoleReserved(roleDefinition)) ?? false; const isDeprecated: boolean = (roleDefinition && isRoleDeprecated(roleDefinition)) ?? false; + const isSystem: boolean = (roleDefinition && isRoleSystem(roleDefinition)) ?? false; + const isAdmin: boolean = (roleDefinition && isRoleAdmin(roleDefinition)) ?? false; return { - color: isDeprecated ? 'warning' : 'default', + color: isDeprecated ? 'warning' : isReserved ? 'primary' : undefined, 'data-test-subj': `roleOption-${roleName}`, label: roleName, value: { + isReserved, isDeprecated, + isSystem, + isAdmin, + deprecatedReason: roleDefinition?.metadata?._deprecated_reason, }, }; }; const options = props.availableRoles.map((role) => roleNameToOption(role.name)); - const selectedOptions = props.selectedRoleNames.map(roleNameToOption); + const groupedOptions = options.reduce>((acc, option) => { + const type = option.value?.isDeprecated + ? 'deprecated' + : option.value?.isSystem + ? 'system' + : option.value?.isAdmin + ? 'admin' + : option.value?.isReserved + ? 'user' + : 'custom'; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(option); + return acc; + }, {}); return ( } + renderOption={renderOption} /> ); }; + +function renderOption(option: Option) { + return ( + + {option.label} + {option.value?.isDeprecated ? ( + + + + + + ) : option.value?.isReserved ? ( + + + + + + ) : undefined} + + ); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx deleted file mode 100644 index b24a48145b461..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { RoleComboBoxOption } from './role_combo_box_option'; - -describe('RoleComboBoxOption', () => { - it('renders a regular role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - - `); - }); - - it('renders a deprecated role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - (deprecated) - - `); - }); -}); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx deleted file mode 100644 index ae9b79c796275..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiComboBoxOptionOption, EuiText } from '@elastic/eui'; - -interface Props { - option: EuiComboBoxOptionOption<{ isDeprecated: boolean }>; -} - -export const RoleComboBoxOption = ({ option }: Props) => { - const isDeprecated = option.value?.isDeprecated ?? false; - const deprecatedLabel = i18n.translate( - 'xpack.security.management.users.editUser.deprecatedRoleText', - { - defaultMessage: '(deprecated)', - } - ); - - return ( - - {option.label} {isDeprecated ? deprecatedLabel : ''} - - ); -}; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index c750ec373b9f7..df5e5c8be9025 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -407,7 +407,7 @@ export const EditRolePage: FunctionComponent = ({ const onNameChange = (e: ChangeEvent) => setRole({ ...role, - name: e.target.value.replace(/\s/g, '_'), + name: e.target.value, }); const getElasticsearchPrivileges = () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts index 868674aec6f86..e6b9b19022f31 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts @@ -40,7 +40,7 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Please provide a role name`, + error: `Please provide a role name.`, }); }); @@ -57,13 +57,30 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Name must not exceed 1024 characters`, + error: `Name must not exceed 1024 characters.`, + }); + }); + + test('it cannot start with whitespace character', () => { + const role = { + name: ' role-name', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + expect(validator.validateRoleName(role)).toEqual({ + isInvalid: true, + error: `Name must not contain leading or trailing spaces.`, }); }); const charList = `!#%^&*()+=[]{}\|';:"/,<>?`.split(''); charList.forEach((element) => { - test(`it cannot support the "${element}" character`, () => { + test(`it allows the "${element}" character`, () => { const role = { name: `role-${element}`, elasticsearch: { @@ -74,10 +91,7 @@ describe('validateRoleName', () => { kibana: [], }; - expect(validator.validateRoleName(role)).toEqual({ - isInvalid: true, - error: `Name must begin with a letter or underscore and contain only letters, underscores, and numbers.`, - }); + expect(validator.validateRoleName(role)).toEqual({ isInvalid: false }); }); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts index 89b16b1467776..e0459bbd3dd0d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Role, RoleIndexPrivilege } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; interface RoleValidatorOptions { shouldValidate?: boolean; @@ -41,25 +42,36 @@ export class RoleValidator { i18n.translate( 'xpack.security.management.editRole.validateRole.provideRoleNameWarningMessage', { - defaultMessage: 'Please provide a role name', + defaultMessage: 'Please provide a role name.', } ) ); } - if (role.name.length > 1024) { + if (role.name.length > MAX_NAME_LENGTH) { return invalid( i18n.translate('xpack.security.management.editRole.validateRole.nameLengthWarningMessage', { - defaultMessage: 'Name must not exceed 1024 characters', + defaultMessage: 'Name must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, }) ); } - if (!role.name.match(/^[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*$/)) { + if (role.name.trim() !== role.name) { + return invalid( + i18n.translate( + 'xpack.security.management.editRole.validateRole.nameWhitespaceWarningMessage', + { + defaultMessage: `Name must not contain leading or trailing spaces.`, + } + ) + ); + } + if (!role.name.match(NAME_REGEX)) { return invalid( i18n.translate( 'xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage', { defaultMessage: - 'Name must begin with a letter or underscore and contain only letters, underscores, and numbers.', + 'Name must contain only letters, numbers, spaces, punctuation and printable symbols.', } ) ); diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx new file mode 100644 index 0000000000000..2586b7c24bf4c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { + EuiCallOut, + EuiFieldPassword, + EuiFlexGroup, + EuiForm, + EuiFormRow, + EuiIcon, + EuiLoadingContent, + EuiSpacer, + EuiText, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { useCurrentUser } from '../../../components/use_current_user'; +import { FormFlyout } from '../../../components/form_flyout'; +import { UserAPIClient } from '..'; + +export interface ChangePasswordFormValues { + current_password?: string; + password: string; + confirm_password: string; +} + +export interface ChangePasswordFlyoutProps { + username: string; + defaultValues?: ChangePasswordFormValues; + onCancel(): void; + onSuccess?(): void; +} + +export const ChangePasswordFlyout: FunctionComponent = ({ + username, + defaultValues = { + current_password: '', + password: '', + confirm_password: '', + }, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + const { value: currentUser, loading: isLoading } = useCurrentUser(); + const isCurrentUser = currentUser?.username === username; + const isSystemUser = username === 'kibana' || username === 'kibana_system'; + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + try { + await new UserAPIClient(services.http!).changePassword( + username, + values.password, + values.current_password + ); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.changePasswordFlyout.successMessage', { + defaultMessage: "Password changed for '{username}'.", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + if ((error as any).body?.message === 'security_exception') { + form.setError( + 'current_password', + i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordInvalidError', + { + defaultMessage: 'Invalid password.', + } + ) + ); + } else { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.errorMessage', + { + defaultMessage: 'Could not change password', + } + ), + text: (error as any).body?.message || error.message, + }); + throw error; + } + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isCurrentUser) { + if (!values.current_password) { + errors.current_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', + { + defaultMessage: 'Enter your current password.', + } + ); + } + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', + { + defaultMessage: 'Enter a new password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + + return errors; + }, + defaultValues, + }); + + return ( + + {isLoading ? ( + + ) : ( + + {isSystemUser ? ( + <> + +

+ +

+

+ +

+
+ + + ) : undefined} + + + + + + + + + {username} + + + + + + {isCurrentUser ? ( + + + + ) : null} + + + + + + + + + {/* Hidden submit button is required for enter key to trigger form submission */} + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx new file mode 100644 index 0000000000000..18be46ebefed0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDeleteUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDeleteUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, deleteUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).deleteUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDeleteUsers.successMessage', { + defaultMessage: "Deleted user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmDeleteUsers.errorMessage', { + defaultMessage: "Could not delete user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx new file mode 100644 index 0000000000000..b0a9e875c2089 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDisableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDisableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + const isSystemUser = usernames[0] === 'kibana' || usernames[0] === 'kibana_system'; + + const [state, disableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).disableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDisableUsers.successMessage', { + defaultMessage: "Deactivated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.confirmDisableUsers.errorMessage', + { + defaultMessage: "Could not deactivate user '{username}'", + values: { username }, + } + ), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + {isSystemUser ? ( + +

+ +

+

+ +

+
+ ) : ( + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx new file mode 100644 index 0000000000000..c9589cfa17da2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmEnableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmEnableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, enableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).enableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmEnableUsers.successMessage', { + defaultMessage: "Activated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmEnableUsers.errorMessage', { + defaultMessage: "Could not activate user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx new file mode 100644 index 0000000000000..e7e3e1164ae14 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, fireEvent, waitFor, within } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { securityMock } from '../../../mocks'; +import { Providers } from '../users_management_app'; +import { CreateUserPage } from './create_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('CreateUserPage', () => { + it('creates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + coreStart.http.post.mockResolvedValue({}); + + const { findByRole, findByLabelText } = render( + + + + ); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'jdoe' } }); + fireEvent.change(await findByLabelText('Password'), { target: { value: 'changeme' } }); + fireEvent.change(await findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + password: 'changeme', + username: 'jdoe', + full_name: '', + email: '', + roles: [], + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.get.mockResolvedValueOnce([ + { + username: 'existing_username', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }, + ]); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + const alert = await findByRole('alert'); + within(alert).getByText(/Enter a username/i); + within(alert).getByText(/Enter a password/i); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'existing_username' } }); + + await findAllByText(/User 'existing_username' already exists/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: ' username_with_leading_space' }, + }); + + await findAllByText(/Username must not contain leading or trailing spaces/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: '€' }, + }); + + await findAllByText( + /Username must contain only letters, numbers, spaces, punctuation, and symbols/i + ); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '111' } }); + + await findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '123456' } }); + fireEvent.change(await findByLabelText('Confirm password'), { target: { value: '111' } }); + + await findAllByText(/Passwords do not match/i); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx new file mode 100644 index 0000000000000..6842ddb774bda --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { + EuiHorizontalRule, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { UserForm } from './user_form'; + +export const CreateUserPage: FunctionComponent = () => { + const history = useHistory(); + const backToUsers = () => history.push('/'); + + return ( + + + + +

+ +

+
+
+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss deleted file mode 100644 index 727fac4782752..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.secUsersEditPage__content { - max-width: 460px; - margin-left: auto; - margin-right: auto; - flex-grow: 0; -} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 5e8c9f2d14a4c..f065c45d7080c 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -4,228 +4,440 @@ * you may not use this file except in compliance with the Elastic License. */ -import { act } from '@testing-library/react'; -import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { User, Role } from '../../../../common/model'; -import { ReactWrapper } from 'enzyme'; -import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; +import { + fireEvent, + render, + waitFor, + waitForElementToBeRemoved, + within, +} from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; -import { rolesAPIClientMock } from '../../roles/index.mock'; -import { userAPIClientMock } from '../index.mock'; -import { findTestSubject } from '@kbn/test/jest'; - -const createUser = (username: string, roles = ['idk', 'something']) => { - const user: User = { - username, - full_name: 'my full name', - email: 'foo@bar.com', - roles, - enabled: true, - }; - - if (username === 'reserved_user') { - user.metadata = { - _reserved: true, - }; - } - - if (username === 'deprecated_user') { - user.metadata = { - _reserved: true, - _deprecated: true, - _deprecated_reason: 'beacuse I said so.', - }; - } - - return user; +import { Providers } from '../users_management_app'; +import { EditUserPage } from './edit_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const userMock = { + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], }; -const buildClients = (user: User) => { - const apiClient = userAPIClientMock.create(); - apiClient.getUser.mockResolvedValue(user); +describe('EditUserPage', () => { + it('warns when viewing deactivated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByText } = render( + + + + ); - const rolesAPIClient = rolesAPIClientMock.create(); - rolesAPIClient.getRoles.mockImplementation(() => { - return Promise.resolve([ - { - name: 'role 1', - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: [], - }, - kibana: [], - }, - { - name: 'role 2', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + await findByText(/User has been deactivated/i); + }); + + it('warns when viewing deprecated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { + _reserved: true, + _deprecated: true, + _deprecated_reason: 'Use [new_user] instead.', }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is deprecated/i); + await findByText(/Use .new_user. instead/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when viewing built-in user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { _reserved: true, _deprecated: false }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is built in/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when selecting deprecated role', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + roles: ['deprecated_role'], + }); + coreStart.http.get.mockResolvedValueOnce([ { - name: 'deprecated-role', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + name: 'deprecated_role', metadata: { + _reserved: true, _deprecated: true, + _deprecated_reason: 'Use [new_role] instead.', }, }, - ] as Role[]); + ]); + + const { findByText } = render( + + + + ); + + await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i); }); - return { apiClient, rolesAPIClient }; -}; + it('updates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; -function buildSecuritySetup() { - const securitySetupMock = securityMock.createSetup(); - securitySetupMock.authc.getCurrentUser.mockResolvedValue( - mockAuthenticatedUser(createUser('current_user')) - ); - return securitySetupMock; -} + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); -function expectSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(1); -} + const { findByRole, findByLabelText } = render( + + + + ); -function expectMissingSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(0); -} + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); -describe('EditUserPage', () => { - const history = scopedHistoryMock.create(); - - it('allows reserved users to be viewed', async () => { - const user = createUser('reserved_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('warns when user form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); + + const { findByRole, findByLabelText } = render( + + + ); - await waitForRender(wrapper); + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: "Could not update user 'jdoe'", + }); + expect(history.location.pathname).toBe('/edit/jdoe'); + }); + }); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + it('changes password of other user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockResolvedValueOnce({}); - expectMissingSaveButton(wrapper); + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = getByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(within(dialog).getByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(within(dialog).getByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + }), + }); }); - it('allows new users to be created', async () => { - const user = createUser(''); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('changes password of current user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + password: '123456', + }), + }); + }); + + it('warns when change password form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - expect(apiClient.getUser).toBeCalledTimes(0); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(0); + const { findByRole } = render( + + + + ); - expectSaveButton(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitFor(() => { + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: 'Could not change password', + }); + }); }); - it('allows existing users to be edited', async () => { - const user = createUser('existing_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('validates change password form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + await within(dialog).findByText(/Enter your current password/i); + await within(dialog).findByText(/Enter a new password/i); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(0); - expectSaveButton(wrapper); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: 'changeme' }, + }); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Passwords do not match/i); }); - it('warns when viewing a depreciated user', async () => { - const user = createUser('deprecated_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('deactivates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Deactivate user' })); - expect(findTestSubject(wrapper, 'deprecatedUserWarning')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); }); - it('warns when user is assigned a deprecated role', async () => { - const user = createUser('existing_user', ['deprecated-role']); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('activates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findAllByRole } = render( + + + ); - await waitForRender(wrapper); + const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); + fireEvent.click(enableButton); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Activate user' })); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); -}); -async function waitForRender(wrapper: ReactWrapper) { - await act(async () => { - await nextTick(); - wrapper.update(); + it('deletes user when confirming and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.delete.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Delete user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); + + expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); + await waitFor(() => { + expect(history.location.pathname).toBe('/'); + }); }); -} +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index dc0c3336cb85f..68c01bf509b0d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -4,615 +4,315 @@ * you may not use this file except in compliance with the Elastic License. */ -import './edit_user_page.scss'; - -import { get } from 'lodash'; -import React, { Component, Fragment, ChangeEvent } from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import { + EuiAvatar, EuiButton, EuiCallOut, - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiLink, - EuiTitle, - EuiForm, - EuiFormRow, - EuiIcon, - EuiText, - EuiFieldText, + EuiHorizontalRule, EuiPageContent, + EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, - EuiHorizontalRule, + EuiPanel, EuiSpacer, + EuiText, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { NotificationsStart, ScopedHistory } from 'src/core/public'; -import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; -import { AuthenticationServiceSetup } from '../../../authentication'; -import { RolesAPIClient } from '../../roles'; -import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; -import { UserValidator, UserValidationResult } from './validate_user'; -import { RoleComboBox } from '../../role_combo_box'; -import { isUserDeprecated, getExtendedUserDeprecationNotice, isUserReserved } from '../user_utils'; +import { useHistory } from 'react-router-dom'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { getUserDisplayName } from '../../../../common/model'; +import { isUserDeprecated, isUserReserved } from '../user_utils'; +import { UserForm } from './user_form'; +import { ChangePasswordFlyout } from './change_password_flyout'; +import { ConfirmDisableUsers } from './confirm_disable_users'; +import { ConfirmEnableUsers } from './confirm_enable_users'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import { UserAPIClient } from '..'; -interface Props { - username?: string; - userAPIClient: PublicMethodsOf; - rolesAPIClient: PublicMethodsOf; - authc: AuthenticationServiceSetup; - notifications: NotificationsStart; - history: ScopedHistory; -} - -interface State { - isLoaded: boolean; - isNewUser: boolean; - currentUser: User | null; - showChangePasswordForm: boolean; - showDeleteConfirmation: boolean; - user: EditUser; - roles: Role[]; - selectedRoles: readonly string[]; - formError: UserValidationResult | null; +export interface EditUserPageProps { + username: string; } -export class EditUserPage extends Component { - private validator: UserValidator; - - constructor(props: Props) { - super(props); - this.validator = new UserValidator({ shouldValidate: false }); - this.state = { - isLoaded: false, - isNewUser: true, - currentUser: null, - showChangePasswordForm: false, - showDeleteConfirmation: false, - user: { - email: '', - username: '', - full_name: '', - roles: [], - enabled: true, - password: '', - confirmPassword: '', - }, - roles: [], - selectedRoles: [], - formError: null, - }; - } - - public async componentDidMount() { - await this.setCurrentUser(); - } - - public async componentDidUpdate(prevProps: Props) { - if (prevProps.username !== this.props.username) { - await this.setCurrentUser(); +export type EditUserPageAction = + | 'changePassword' + | 'disableUser' + | 'enableUser' + | 'deleteUser' + | 'none'; + +export const EditUserPage: FunctionComponent = ({ username }) => { + const { services } = useKibana(); + const history = useHistory(); + const [{ value: user, error }, getUser] = useAsyncFn( + () => new UserAPIClient(services.http!).getUser(username), + [services.http] + ); + const [action, setAction] = useState('none'); + + const backToUsers = () => history.push('/'); + + useEffect(() => { + getUser(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (error) { + backToUsers(); } - } + }, [error]); // eslint-disable-line react-hooks/exhaustive-deps - private backToUserList() { - this.props.history.push('/'); + if (!user) { + return null; } - private async setCurrentUser() { - const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props; - let { user, currentUser } = this.state; - if (username) { - try { - user = { - ...(await userAPIClient.getUser(username)), - password: '', - confirmPassword: '', - }; - currentUser = await authc.getCurrentUser(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { - defaultMessage: 'Error loading user', - }), - text: get(err, 'body.message') || err.message, - }); - return this.backToUserList(); - } - } - - let roles: Role[] = []; - try { - roles = await rolesAPIClient.getRoles(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { - defaultMessage: 'Error loading roles', - }), - text: get(err, 'body.message') || err.message, - }); - } - - this.setState({ - isLoaded: true, - isNewUser: !username, - currentUser, - user, - roles, - selectedRoles: user.roles || [], - }); - } - - private handleDelete = (usernames: string[], errors: string[]) => { - if (errors.length === 0) { - this.backToUserList(); - } - }; - - private saveUser = async () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.user, this.state.isNewUser); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - const { userAPIClient } = this.props; - const { user, isNewUser, selectedRoles } = this.state; - const userToSave: EditUser = { ...user }; - if (!isNewUser) { - delete userToSave.password; - } - delete userToSave.confirmPassword; - userToSave.roles = [...selectedRoles]; - try { - await userAPIClient.saveUser(userToSave); - this.props.notifications.toasts.addSuccess( - i18n.translate( - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', - { - defaultMessage: 'Saved user {message}', - values: { message: user.username }, - } - ) - ); - - this.backToUserList(); - } catch (e) { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { - defaultMessage: 'Error saving user: {message}', - values: { message: get(e, 'body.message', 'Unknown error') }, - }) - ); - } - } - }; - - private passwordFields = () => { - return ( - - - - - - - - - ); - }; - - private changePasswordForm = () => { - const { showChangePasswordForm, user, currentUser } = this.state; - - const userIsLoggedInUser = Boolean( - currentUser && user.username && user.username === currentUser.username - ); - - if (!showChangePasswordForm) { - return null; - } - return ( - + const isReservedUser = isUserReserved(user); + const isDeprecatedUser = isUserDeprecated(user); + const displayName = getUserDisplayName(user); + + return ( + + + + + + + + + +

{displayName}

+
+ {user.email} +
+
+
+
+ - {user.username === 'kibana' || user.username === 'kibana_system' ? ( - + {isDeprecatedUser ? ( + <> + } + iconType="alert" color="warning" - iconType="help" > -

+ {user.metadata?._deprecated_reason?.replace(/\[(.+)\]/, "'$1'")} + + + + ) : isReservedUser ? ( + <> + + } + iconType="lock" + /> + + + ) : user.enabled === false ? ( + <> + -

+ } + > + setAction('enableUser')} size="s"> + +
-
- ) : null} - + ) : undefined} + + -
- ); - }; - - private toggleChangePasswordForm = () => { - const { showChangePasswordForm } = this.state; - this.setState({ showChangePasswordForm: !showChangePasswordForm }); - }; - - private onUsernameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - username: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - private onEmailChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - email: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onFullNameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - full_name: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - password: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onConfirmPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - confirmPassword: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onRolesChange = (selectedRoles: string[]) => { - this.setState({ - selectedRoles, - }); - }; - - private cannotSaveUser = () => { - const { user, isNewUser } = this.state; - const result = this.validator.validateForSave(user, isNewUser); - return result.isInvalid; - }; - - private onCancelDelete = () => { - this.setState({ showDeleteConfirmation: false }); - }; - - public render() { - const { - user, - selectedRoles, - roles, - showChangePasswordForm, - isNewUser, - showDeleteConfirmation, - } = this.state; - const reserved = isUserReserved(user); - if (!user || !roles) { - return null; - } - - if (!this.state.isLoaded) { - return null; - } - - const hasAnyDeprecatedRolesAssigned = selectedRoles.some((selected) => { - const role = roles.find((r) => r.name === selected); - return role && isRoleDeprecated(role); - }); + {action === 'changePassword' ? ( + setAction('none')} + onSuccess={() => setAction('none')} + /> + ) : action === 'disableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'enableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'deleteUser' ? ( + setAction('none')} + onSuccess={backToUsers} + /> + ) : undefined} - const roleHelpText = hasAnyDeprecatedRolesAssigned ? ( - - - - ) : undefined; + + - return ( -
- - - - -

- {isNewUser ? ( + + + + + + + + + + + + + + setAction('changePassword')} size="s"> + + + + + + + + {user.enabled === false ? ( + + + + + - ) : ( + + - )} -

-
-
- {reserved && ( - - - - )} -
- - {reserved && ( - - -

+ + + + + setAction('enableUser')} size="s"> + + + + + + ) : ( + + + + + -

-
- -
- )} - - {isUserDeprecated(user) && ( - - - - - )} - - {showDeleteConfirmation ? ( - - ) : null} - - - - - - {isNewUser ? this.passwordFields() : null} - {reserved ? null : ( - - - - - - - - - )} - - - - - {isNewUser || showChangePasswordForm ? null : ( - - + + - - - )} - {this.changePasswordForm()} - - - - {reserved && ( - this.backToUserList()}> + + + + + setAction('disableUser')} size="s"> - )} - {reserved ? null : ( - - - this.saveUser()} - > - {isNewUser ? ( - - ) : ( - - )} - - - - this.backToUserList()} - > + + + + )} + + {!isReservedUser && ( + <> + + + + + + - - - - {isNewUser || reserved ? null : ( - - { - this.setState({ showDeleteConfirmation: true }); - }} - data-test-subj="userFormDeleteButton" - color="danger" - > - - - - )} - - )} - -
-
-
- ); - } -} + + + + + + + + setAction('deleteUser')} size="s" color="danger"> + + + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts index 92eb17b9ebd36..30069d8e97c31 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/index.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/index.ts @@ -5,3 +5,4 @@ */ export { EditUserPage } from './edit_user_page'; +export { CreateUserPage } from './create_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx new file mode 100644 index 0000000000000..daa488d674fbb --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -0,0 +1,466 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useEffect, useCallback } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldPassword, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { throttle } from 'lodash'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { User, Role, isRoleDeprecated } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { DocLink } from '../../../components/doc_link'; +import { RolesAPIClient } from '../../roles'; +import { RoleComboBox } from '../../role_combo_box'; +import { UserAPIClient } from '..'; + +export const THROTTLE_USERS_WAIT = 10000; + +export interface UserFormValues { + username?: string; + full_name: string; + email: string; + password?: string; + confirm_password?: string; + roles: readonly string[]; +} + +export interface UserFormProps { + isNewUser?: boolean; + isReservedUser?: boolean; + isCurrentUser?: boolean; + defaultValues?: UserFormValues; + onCancel(): void; + onSuccess?(): void; +} + +const defaultDefaultValues: UserFormValues = { + username: '', + password: '', + confirm_password: '', + full_name: '', + email: '', + roles: [], +}; + +export const UserForm: FunctionComponent = ({ + isNewUser = false, + isReservedUser = false, + defaultValues = defaultDefaultValues, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + + const [rolesState, getRoles] = useAsyncFn(() => new RolesAPIClient(services.http!).getRoles(), [ + services.http, + ]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const getUsersThrottled = useCallback( + throttle(() => new UserAPIClient(services.http!).getUsers(), THROTTLE_USERS_WAIT), + [services.http] + ); + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { password, confirm_password, ...rest } = values; + const user = isNewUser ? { password, ...rest } : rest; + try { + await new UserAPIClient(services.http!).saveUser(user as User); + services.notifications!.toasts.addSuccess( + isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createSuccessMessage', { + defaultMessage: "Created user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateSuccessMessage', { + defaultMessage: "Updated user '{username}'", + values: { username: user.username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createErrorMessage', { + defaultMessage: "Could not create user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateErrorMessage', { + defaultMessage: "Could not update user '{username}'", + values: { username: user.username }, + }), + text: (error as any).body?.message || error.message, + }); + throw error; + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isNewUser) { + if (!values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameRequiredError', + { + defaultMessage: 'Enter a username.', + } + ); + } else if (values.username.length > MAX_NAME_LENGTH) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameMaxLengthError', + { + defaultMessage: 'Username must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, + } + ); + } else if (values.username.trim() !== values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameWhitespaceError', + { + defaultMessage: `Username must not contain leading or trailing spaces.`, + } + ); + } else if (!values.username.match(NAME_REGEX)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameInvalidError', + { + defaultMessage: + 'Username must contain only letters, numbers, spaces, punctuation, and symbols.', + } + ); + } else { + try { + const users = await getUsersThrottled(); + if (users.some((user) => user.username === values.username)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameTakenError', + { + defaultMessage: "User '{username}' already exists.", + values: { username: values.username }, + } + ); + } + } catch (error) {} // eslint-disable-line no-empty + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordRequiredError', + { + defaultMessage: 'Enter a password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + } + + return errors; + }, + defaultValues, + }); + + useEffect(() => { + form.reset(defaultValues); + }, [defaultValues]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + getRoles(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const availableRoles = rolesState.value ?? []; + const selectedRoleNames = form.values.roles ?? []; + const deprecatedRoles = selectedRoleNames.reduce((roles, name) => { + const role = availableRoles.find((r) => r.name === name); + if (role && isRoleDeprecated(role)) { + roles.push(role); + } + return roles; + }, []); + + return ( + + + + + } + description={i18n.translate('xpack.security.management.users.userForm.profileDescription', { + defaultMessage: 'Provide personal details.', + })} + > + + + + + {!isReservedUser ? ( + <> + + + + + + + + ) : undefined} + + + {isNewUser ? ( + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.passwordDescription', + { + defaultMessage: 'Protect your data with a strong password.', + } + )} + > + + + + + + + + ) : null} + + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.privilegesDescription', + { + defaultMessage: 'Assign roles to manage access and permissions.', + } + )} + > + 0 ? ( + + {deprecatedRoles.map((role) => ( +

+ +

+ ))} +
+ ) : ( + + + + ) + } + > + form.setValue('roles', value)} + isLoading={rolesState.loading} + isDisabled={isReservedUser} + /> +
+ + + {isReservedUser ? ( + + + + + + + + ) : ( + + + + {isNewUser ? ( + + ) : ( + + )} + + + + + + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts deleted file mode 100644 index 6050e1868a759..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../../../common/model'; - -function expectValid(result: UserValidationResult) { - expect(result.isInvalid).toBe(false); -} - -function expectInvalid(result: UserValidationResult) { - expect(result.isInvalid).toBe(true); -} - -describe('UserValidator', () => { - describe('#validateUsername', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateUsername({} as User)); - }); - - it(`returns 'invalid' if username is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validateUsername({} as User)); - }); - - it(`returns 'invalid' if username contains invalid characters`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: '!@#$%^&*()', - } as User) - ); - }); - - it(`returns 'valid' for correct usernames`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: 'my_user', - } as User) - ); - }); - }); - - describe('#validateEmail', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateEmail({} as EditUser)); - }); - - it(`returns 'valid' if email is missing`, () => { - expectValid(new UserValidator({ shouldValidate: true }).validateEmail({} as EditUser)); - }); - - it(`returns 'invalid' for invalid emails`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'asf', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct emails`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'foo@bar.co', - } as EditUser) - ); - }); - }); - - describe('#validatePassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' if password is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' for invalid password`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'short', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'changeme', - } as EditUser) - ); - }); - }); - - describe('#validateConfirmPassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateConfirmPassword({} as EditUser)); - }); - - it(`returns 'invalid' if confirm password is missing`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - } as EditUser) - ); - }); - - it(`returns 'invalid' for mismatched passwords`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeyou', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeme', - } as EditUser) - ); - }); - }); -}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts deleted file mode 100644 index 5edd96c68bf0d..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../../../common/model'; - -interface UserValidatorOptions { - shouldValidate?: boolean; -} - -export interface UserValidationResult { - isInvalid: boolean; - error?: string; -} - -const validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const validUsernameRegex = /[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*/; - -export class UserValidator { - private shouldValidate?: boolean; - - constructor(options: UserValidatorOptions = {}) { - this.shouldValidate = options.shouldValidate; - } - - public enableValidation() { - this.shouldValidate = true; - } - - public disableValidation() { - this.shouldValidate = false; - } - - public validateUsername(user: User): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { username } = user; - if (!username) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.requiredUsernameErrorMessage', { - defaultMessage: 'Username is required', - }) - ); - } else if (username && !username.match(validUsernameRegex)) { - return invalid( - i18n.translate( - 'xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage', - { - defaultMessage: - 'Username must begin with a letter or underscore and contain only letters, underscores, and numbers', - } - ) - ); - } - - return valid(); - } - - public validateEmail(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { email } = user; - if (email && !email.match(validEmailRegex)) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.validEmailRequiredErrorMessage', { - defaultMessage: 'Email address is invalid', - }) - ); - } - return valid(); - } - - public validatePassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password } = user; - if (!password || password.length < 6) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordLengthErrorMessage', { - defaultMessage: 'Password must be at least 6 characters', - }) - ); - } - return valid(); - } - - public validateConfirmPassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password, confirmPassword } = user; - if (password && confirmPassword !== null && password !== confirmPassword) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage', { - defaultMessage: 'Passwords do not match', - }) - ); - } - return valid(); - } - - public validateForSave(user: EditUser, isNewUser: boolean): UserValidationResult { - const { isInvalid: isUsernameInvalid } = this.validateUsername(user); - const { isInvalid: isEmailInvalid } = this.validateEmail(user); - let isPasswordInvalid = false; - let isConfirmPasswordInvalid = false; - - if (isNewUser) { - isPasswordInvalid = this.validatePassword(user).isInvalid; - isConfirmPasswordInvalid = this.validateConfirmPassword(user).isInvalid; - } - - if (isUsernameInvalid || isEmailInvalid || isPasswordInvalid || isConfirmPasswordInvalid) { - return invalid(); - } - - return valid(); - } -} - -function invalid(error?: string): UserValidationResult { - return { - isInvalid: true, - error, - }; -} - -function valid(): UserValidationResult { - return { - isInvalid: false, - }; -} diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts index 7223f78d57fdc..54c7ae8f4ae3b 100644 --- a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts @@ -9,6 +9,8 @@ export const userAPIClientMock = { getUsers: jest.fn(), getUser: jest.fn(), deleteUser: jest.fn(), + enableUser: jest.fn(), + disableUser: jest.fn(), saveUser: jest.fn(), changePassword: jest.fn(), }), diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts index 61dd09d2c5e3d..b96596ba7c653 100644 --- a/x-pack/plugins/security/public/management/users/user_api_client.ts +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -30,7 +30,7 @@ export class UserAPIClient { }); } - public async changePassword(username: string, password: string, currentPassword: string) { + public async changePassword(username: string, password: string, currentPassword?: string) { const data: Record = { newPassword: password, }; @@ -42,4 +42,12 @@ export class UserAPIClient { body: JSON.stringify(data), }); } + + public async disableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_disable`); + } + + public async enableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_enable`); + } } diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index 37747f9a1ccfa..3b1705d2bc46b 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -237,7 +237,7 @@ export class UsersGridPage extends Component { ({ - UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, -})); - -jest.mock('./edit_user', () => ({ - EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, -})); - -import { usersManagementApp } from './users_management_app'; - import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { securityMock } from '../../mocks'; +import { usersManagementApp } from './users_management_app'; -async function mountApp(basePath: string, pathname: string) { - const container = document.createElement('div'); - const setBreadcrumbs = jest.fn(); +const element = document.body.appendChild(document.createElement('div')); - const unmount = await usersManagementApp - .create({ - authc: securityMock.createSetup().authc, - getStartServices: coreMock.createSetup().getStartServices as any, - }) - .mount({ - basePath, - element: container, +describe('usersManagementApp', () => { + it('renders application and sets breadcrumbs', async () => { + const { getStartServices } = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + getStartServices.mockResolvedValue([coreStartMock, {}, {}]); + const { authc } = securityMock.createSetup(); + + const setBreadcrumbs = jest.fn(); + const history = scopedHistoryMock.create({ pathname: '/create' }); + + const unmount = await usersManagementApp.create({ authc, getStartServices }).mount({ + basePath: '/', + element, setBreadcrumbs, - history: scopedHistoryMock.create({ pathname }), + history, }); - return { unmount, container, setBreadcrumbs }; -} - -describe('usersManagementApp', () => { - it('create() returns proper management app descriptor', () => { - expect( - usersManagementApp.create({ - authc: securityMock.createSetup().authc, - getStartServices: coreMock.createSetup().getStartServices as any, - }) - ).toMatchInlineSnapshot(` - Object { - "id": "users", - "mount": [Function], - "order": 10, - "title": "Users", - } - `); - }); - - it('mount() works for the `grid` page', async () => { - const { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }]); - expect(container).toMatchInlineSnapshot(` -
- Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `create user` page', async () => { - const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }, { text: 'Create' }]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `edit user` page', async () => { - const userName = 'foo@bar.com'; - - const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { href: `/edit/${encodeURIComponent(userName)}`, text: userName }, + expect(setBreadcrumbs).toHaveBeenLastCalledWith([ + { href: '/', text: 'Users' }, + { href: '/create', text: 'Create' }, ]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"username":"foo@bar.com","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/foo@bar.com","search":"","hash":""}}} -
- `); unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - const usernames = ['foo@bar.com', 'foo&bar.com', 'some 安全性 user']; - usernames.forEach((username) => { - it( - 'mount() properly encodes user name in `edit user` page link in breadcrumbs for user ' + - username, - async () => { - const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { - href: `/edit/${encodeURIComponent(username)}`, - text: username, - }, - ]); - } - ); }); }); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 2f16f85d5fcae..cbb303d1a128d 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -4,14 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, Redirect, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor } from 'src/core/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; +import { + BreadcrumbsProvider, + BreadcrumbsChangeHandler, + Breadcrumb, + getDocTitle, +} from '../../components/breadcrumb'; +import { AuthenticationProvider } from '../../components/use_current_user'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -19,6 +29,10 @@ interface CreateParams { getStartServices: StartServicesAccessor; } +interface EditUserParams { + username: string; +} + export const usersManagementApp = Object.freeze({ id: 'users', create({ authc, getStartServices }: CreateParams) { @@ -27,18 +41,10 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ element, setBreadcrumbs, history }) { - const [coreStart] = await getStartServices(); - const usersBreadcrumbs = [ - { - text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), - href: `/`, - }, - ]; - const [ - [{ http, notifications, i18n: i18nStart }], + [coreStart], { UsersGridPage }, - { EditUserPage }, + { CreateUserPage, EditUserPage }, { UserAPIClient }, { RolesAPIClient }, ] = await Promise.all([ @@ -49,64 +55,61 @@ export const usersManagementApp = Object.freeze({ import('../roles'), ]); - const userAPIClient = new UserAPIClient(http); - const rolesAPIClient = new RolesAPIClient(http); - const UsersGridPageWithBreadcrumbs = () => { - setBreadcrumbs(usersBreadcrumbs); - return ( - - ); - }; - - const EditUserPageWithBreadcrumbs = () => { - const { username } = useParams<{ username?: string }>(); - - // Additional decoding is a workaround for a bug in react-router's version of the `history` module. - // See https://github.com/elastic/kibana/issues/82440 - const decodedUsername = username ? tryDecodeURIComponent(username) : undefined; - - setBreadcrumbs([ - ...usersBreadcrumbs, - username - ? { text: decodedUsername, href: `/edit/${encodeURIComponent(username)}` } - : { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]); - - return ( - - ); - }; - render( - - + { + setBreadcrumbs(breadcrumbs); + coreStart.chrome.docTitle.change(getDocTitle(breadcrumbs)); + }} + > + - + - - + + + + + + ) => { + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const username = tryDecodeURIComponent(props.match.params.username); + return ( + + + + ); + }} + /> + + - - , + + , element ); @@ -117,3 +120,28 @@ export const usersManagementApp = Object.freeze({ } as RegisterManagementAppArgs; }, }); + +export interface ProvidersProps { + services: CoreStart; + history: History; + authc: AuthenticationServiceSetup; + onChange?: BreadcrumbsChangeHandler; +} + +export const Providers: FunctionComponent = ({ + services, + history, + authc, + onChange, + children, +}) => ( + + + + + {children} + + + + +); diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index a98848a583500..bdb6e89719037 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -30,12 +30,7 @@ export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams try { await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, - // Omit `username`, `enabled` and all fields with `null` value. - body: Object.fromEntries( - Object.entries(request.body).filter( - ([key, value]) => value !== null && key !== 'enabled' && key !== 'username' - ) - ), + body: request.body, }); return response.ok({ body: request.body }); diff --git a/x-pack/plugins/security/server/routes/users/disable.ts b/x-pack/plugins/security/server/routes/users/disable.ts new file mode 100644 index 0000000000000..45e1f63149e1a --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/disable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineDisableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_disable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.disableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/enable.ts b/x-pack/plugins/security/server/routes/users/enable.ts new file mode 100644 index 0000000000000..0f4e15c953a42 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/enable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineEnableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_enable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.enableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/index.ts b/x-pack/plugins/security/server/routes/users/index.ts index 931af0734b416..473b3459ad4e1 100644 --- a/x-pack/plugins/security/server/routes/users/index.ts +++ b/x-pack/plugins/security/server/routes/users/index.ts @@ -9,6 +9,8 @@ import { defineGetUserRoutes } from './get'; import { defineGetAllUsersRoutes } from './get_all'; import { defineCreateOrUpdateUserRoutes } from './create_or_update'; import { defineDeleteUserRoutes } from './delete'; +import { defineDisableUserRoutes } from './disable'; +import { defineEnableUserRoutes } from './enable'; import { defineChangeUserPasswordRoutes } from './change_password'; export function defineUsersRoutes(params: RouteDefinitionParams) { @@ -16,5 +18,7 @@ export function defineUsersRoutes(params: RouteDefinitionParams) { defineGetAllUsersRoutes(params); defineCreateOrUpdateUserRoutes(params); defineDeleteUserRoutes(params); + defineDisableUserRoutes(params); + defineEnableUserRoutes(params); defineChangeUserPasswordRoutes(params); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1bbf4b8033755..3a579adcf88c2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16894,7 +16894,7 @@ "xpack.security.management.editRole.updateRoleText": "ロールを更新", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} は数列でなければなりません", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名前は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は 1024 文字以内でなければなりません", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は {maxLength} 文字以内でなければなりません", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "権限が最低 1 つ必要です", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "スペースが最低 1 つ必要です", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "権限が必要です", @@ -17073,37 +17073,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "ユーザーを作成", "xpack.security.management.users.deleteUsersButtonLabel": "{numSelected} 人のユーザー{numSelected, plural, one { } other {s}} 削除", "xpack.security.management.users.deniedPermissionTitle": "ユーザーを管理するにはパーミッションが必要です", - "xpack.security.management.users.editUser.addRolesPlaceholder": "ロールを追加", - "xpack.security.management.users.editUser.cancelButtonLabel": "キャンセル", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "パスワードを変更", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "追加ステップが必要です", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "{username}ユーザーのパスワードを変更後、{kibana}ファイルを更新し、Kibanaを再起動する必要があります。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "ユーザー名は作成後変更できません。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "パスワードの確認", - "xpack.security.management.users.editUser.createUserButtonLabel": "ユーザーを作成", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "ユーザーを削除", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "このユーザーには非推奨ロールが割り当てられています。サポートされているロールに移行してください。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(非推奨)", - "xpack.security.management.users.editUser.editUserTitle": "ユーザー {userName} の編集", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "メールアドレス", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "ロールの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "ユーザーの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "フルネーム", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "リザーブされたユーザーはビルトインのため削除または変更できません。パスワードのみ変更できます。", - "xpack.security.management.users.editUser.newUserTitle": "新規ユーザー", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "パスワードが一致しません", - "xpack.security.management.users.editUser.passwordFormRowLabel": "パスワード", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "パスワードは最低 6 文字必要です", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "ユーザー名が必要です", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "ユーザーリストに戻る", - "xpack.security.management.users.editUser.rolesFormRowLabel": "ロール", - "xpack.security.management.users.editUser.savingUserErrorMessage": "ユーザーの保存中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "パスワードの設定中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "ユーザーを更新", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "ユーザー名は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます", - "xpack.security.management.users.editUser.usernameFormRowLabel": "ユーザー名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "保存されたユーザー {message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "メールアドレスが無効です", "xpack.security.management.users.emailAddressColumnName": "メールアドレス", "xpack.security.management.users.extendedUserDeprecationNotice": "{username}ユーザーは推奨されません。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "ユーザーの取得中にエラーが発生: {message}", @@ -17140,7 +17109,6 @@ "xpack.security.roles.breadcrumb": "ロール", "xpack.security.roles.createBreadcrumb": "作成", "xpack.security.users.breadcrumb": "ユーザー", - "xpack.security.users.createBreadcrumb": "作成", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "プレス", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "値でフィルター", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "値を除外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 51205a3420be5..9099fa0267ba9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16938,7 +16938,7 @@ "xpack.security.management.editRole.updateRoleText": "更新角色", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为数组", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 {maxLength} 个字符", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "“权限”必填", @@ -17117,37 +17117,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "创建用户", "xpack.security.management.users.deleteUsersButtonLabel": "删除 {numSelected} 个用户{numSelected, plural, one { } other { 个用户}}", "xpack.security.management.users.deniedPermissionTitle": "您需要用于管理用户的权限", - "xpack.security.management.users.editUser.addRolesPlaceholder": "添加角色", - "xpack.security.management.users.editUser.cancelButtonLabel": "取消", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "更改密码", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "需要额外的步骤", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "更改 {username} 用户的密码后,必须更新 {kibana} 文件并重新启动 Kibana。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "用户名一经创建,将无法更改。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "确认密码", - "xpack.security.management.users.editUser.createUserButtonLabel": "创建用户", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "删除用户", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "为此用户分配了过时的角色。请迁移到支持的角色。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(已过时)", - "xpack.security.management.users.editUser.editUserTitle": "编辑 {userName} 用户", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "电子邮件地址", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "加载角色时出错", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "加载用户时出错", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "全名", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "保留用户是内置用户,无法删除或修改。只能更改密码。", - "xpack.security.management.users.editUser.newUserTitle": "新建用户", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "密码不匹配", - "xpack.security.management.users.editUser.passwordFormRowLabel": "密码", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "密码长度必须至少为 6 个字符", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "“用户名”必填", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "返回到用户列表", - "xpack.security.management.users.editUser.rolesFormRowLabel": "角色", - "xpack.security.management.users.editUser.savingUserErrorMessage": "保存用户时出错:{message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "设置密码时出错:{message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "更新用户", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "用户名必须以字母或下划线开头,并只能包含字母、下划线和数字", - "xpack.security.management.users.editUser.usernameFormRowLabel": "用户名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "已保存用户{message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "电子邮件地址无效", "xpack.security.management.users.emailAddressColumnName": "电子邮件地址", "xpack.security.management.users.extendedUserDeprecationNotice": "用户 {username} 已过时。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "提取用户时出错:{message}", @@ -17184,7 +17153,6 @@ "xpack.security.roles.breadcrumb": "角色", "xpack.security.roles.createBreadcrumb": "创建", "xpack.security.users.breadcrumb": "用户", - "xpack.security.users.createBreadcrumb": "创建", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "按", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "筛留值", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "筛除值", diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index efdcf4f3f022f..ede120ca43de7 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); + const find = getService('find'); const retry = getService('retry'); describe('Kibana users page a11y tests', () => { @@ -52,24 +53,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test for roles drop down', async () => { - await testSubjects.setValue('userFormUserNameInput', 'a11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'a11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); + await PageObjects.security.clickElasticsearchUsers(); + await PageObjects.security.clickCreateNewUser(); + await PageObjects.security.fillUserForm({ + username: 'a11y', + password: 'password', + confirm_password: 'password', + full_name: 'a11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('rolesDropdown'); await a11y.testAppSnapshot(); }); - it('a11y test for display of delete button on users page ', async () => { - await testSubjects.setValue('userFormUserNameInput', 'deleteA11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'DeleteA11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await testSubjects.click('rolesDropdown'); - await testSubjects.setValue('rolesDropdown', 'roleOption-apm_user'); - await testSubjects.click('userFormSaveButton'); + it('a11y test for display of delete button on users page', async () => { + await PageObjects.security.createUser({ + username: 'deleteA11y', + password: 'password', + confirm_password: 'password', + full_name: 'DeleteA11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('checkboxSelectRow-deleteA11y'); await a11y.testAppSnapshot(); }); @@ -77,17 +83,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for delete user panel ', async () => { await testSubjects.click('deleteUserButton'); await a11y.testAppSnapshot(); + await testSubjects.click('confirmModalCancelButton'); }); it('a11y test for edit user panel', async () => { - await testSubjects.click('confirmModalCancelButton'); await PageObjects.settings.clickLinkText('deleteA11y'); await a11y.testAppSnapshot(); }); - it('a11y test for Change password screen', async () => { + it('a11y test for change password screen', async () => { + await PageObjects.settings.clickLinkText('deleteA11y'); + await find.clickByButtonText('Change password'); + await a11y.testAppSnapshot(); + await testSubjects.click('formFlyoutCancelButton'); + }); + + it('a11y test for deactivate user screen', async () => { await PageObjects.settings.clickLinkText('deleteA11y'); - await testSubjects.click('changePassword'); + await find.clickByButtonText('Deactivate user'); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index 2d112215f4fc1..9084e635f8109 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges')); }); } diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index 191523e969717..6872f423fe630 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges_basic')); }); } diff --git a/x-pack/test/api_integration/apis/security/users.ts b/x-pack/test/api_integration/apis/security/users.ts new file mode 100644 index 0000000000000..e177cf998beee --- /dev/null +++ b/x-pack/test/api_integration/apis/security/users.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const security = getService('security'); + const es = getService('es'); + + const mockUserName = 'test-user'; + const mockUserPassword = 'test-password'; + + describe('Users', () => { + beforeEach(async () => { + await security.user.create(mockUserName, { password: mockUserPassword, roles: [] }); + }); + + afterEach(async () => { + await security.user.delete(mockUserName); + }); + + it('should disable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_disable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(false); + }); + + it('should enable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_enable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 00183113a4d59..b2ddf7d47b1f1 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -68,46 +68,36 @@ export default function ({ getService, getPageObjects }) { before('Create dashboard only mode user', async () => { await PageObjects.settings.navigateTo(); - await PageObjects.security.clickUsersSection(); - await PageObjects.security.clickCreateNewUser(); - await testSubjects.setValue('userFormUserNameInput', 'dashuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'dashuser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'dashuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'dashuser', + roles: ['kibana_dashboard_only_user', 'logstash-data'], + }); }); before('Create user with mixes roles', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mixeduser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_admin'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mixeduser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'kibana_admin', 'logstash-data'], + }); }); before('Create user with dashboard and superuser role', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mysuperuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('superuser'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mysuperuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'superuser'], + }); }); after(async () => { diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 0595322ad2d21..a76475fbbbd8c 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -51,14 +51,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user userEAST ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'userEast', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'dls EAST', + confirm_password: 'changeme', + full_name: 'dls EAST', email: 'dlstest@elastic.com', - save: true, roles: ['kibana_admin', 'myroleEast'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 3f3984dd05a94..a4e2680c394ee 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -71,14 +71,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer1 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer1', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer one', + confirm_password: 'changeme', + full_name: 'customer one', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_viewssnrole'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -87,14 +85,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer2 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer2', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer two', + confirm_password: 'changeme', + full_name: 'customer two', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 58c72eaa3072e..de4515c501187 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -58,13 +58,12 @@ export default function ({ getService, getPageObjects }) { ], }, }); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanauser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanafirst kibanalast', + confirm_password: 'changeme', + full_name: 'kibanafirst kibanalast', email: 'kibanauser@myEmail.com', save: true, roles: ['rbac_all'], @@ -76,13 +75,12 @@ export default function ({ getService, getPageObjects }) { expect(users.kibanauser.roles).to.eql(['rbac_all']); expect(users.kibanauser.fullname).to.eql('kibanafirst kibanalast'); expect(users.kibanauser.reserved).to.be(false); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanareadonly', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanareadonlyFirst kibanareadonlyLast', + confirm_password: 'changeme', + full_name: 'kibanareadonlyFirst kibanareadonlyLast', email: 'kibanareadonly@myEmail.com', save: true, roles: ['rbac_read'], diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 96f16aebd11b9..6f76367801536 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -9,7 +9,7 @@ import { parse } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common', 'roleMappings']); + const pageObjects = getPageObjects(['common', 'security', 'roleMappings']); const security = getService('security'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -32,8 +32,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('allows a role mapping to be created', async () => { await testSubjects.click('createRoleMappingButton'); await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping'); - await testSubjects.setValue('rolesDropdown', 'superuser'); - await browser.pressKeys(browser.keys.ENTER); + await pageObjects.security.selectRole('superuser'); await testSubjects.click('roleMappingsAddRuleButton'); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index c547657bf880a..830d8384f1e3d 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -51,15 +51,13 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Rashmi', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'RashmiFirst RashmiLast', + confirm_password: 'changeme', + full_name: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', - save: true, roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index a2a2b705172d7..c05220b6a59f3 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -19,13 +19,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'newuser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'newuserFirst newuserLast', + confirm_password: 'changeme', + full_name: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', - save: true, roles: ['kibana_admin', 'superuser'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 4fd4384a93c59..7f2b0cfd96ca2 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -41,13 +41,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Lee', password: 'LeePwd', - confirmPassword: 'LeePwd', - fullname: 'LeeFirst LeeLast', + confirm_password: 'LeePwd', + full_name: 'LeeFirst LeeLast', email: 'lee@myEmail.com', - save: true, roles: ['kibana_admin'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -59,11 +58,10 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user with optional fields left empty', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'OptionalUser', password: 'OptionalUserPwd', - confirmPassword: 'OptionalUserPwd', - save: true, + confirm_password: 'OptionalUserPwd', roles: [], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index cad5e29528e9c..868d8115e7f0f 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -7,6 +7,7 @@ import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../ftr_provider_context'; import { AuthenticatedUser, Role } from '../../../plugins/security/common/model'; +import type { UserFormValues } from '../../../plugins/security/public/management/users/edit_user/user_form'; export function SecurityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); @@ -275,7 +276,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickCancelEditUser() { - await testSubjects.click('userFormCancelButton'); + await find.clickByButtonText('Cancel'); } async clickCancelEditRole() { @@ -283,7 +284,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickSaveEditUser() { - await testSubjects.click('userFormSaveButton'); + await find.clickByButtonText('Update user'); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -380,53 +381,58 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider return roles; } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewUser() { return await testSubjects.click('createUserButton'); } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewRole() { return await testSubjects.click('createRoleButton'); } - async addUser(userObj: { - username: string; - password: string; - confirmPassword: string; - email: string; - fullname: string; - roles: string[]; - save?: boolean; - }) { - const self = this; - await this.clickNewUser(); - log.debug('username = ' + userObj.username); - await testSubjects.setValue('userFormUserNameInput', userObj.username); - await testSubjects.setValue('passwordInput', userObj.password); - await testSubjects.setValue('passwordConfirmationInput', userObj.confirmPassword); - if (userObj.fullname) { - await testSubjects.setValue('userFormFullNameInput', userObj.fullname); + async fillUserForm(user: UserFormValues) { + if (user.username) { + await find.setValue('[name=username]', user.username); + } + if (user.password) { + await find.setValue('[name=password]', user.password); + } + if (user.confirm_password) { + await find.setValue('[name=confirm_password]', user.confirm_password); } - if (userObj.email) { - await testSubjects.setValue('userFormEmailInput', userObj.email); + if (user.full_name) { + await find.setValue('[name=full_name]', user.full_name); + } + if (user.email) { + await find.setValue('[name=email]', user.email); } - log.debug('Add roles: ', userObj.roles); - const rolesToAdd = userObj.roles || []; + const rolesToAdd = user.roles || []; for (let i = 0; i < rolesToAdd.length; i++) { - await self.selectRole(rolesToAdd[i]); - } - log.debug('After Add role: , userObj.roleName'); - if (userObj.save === true) { - await testSubjects.click('userFormSaveButton'); - } else { - await testSubjects.click('userFormCancelButton'); + await this.selectRole(rolesToAdd[i]); } } + async submitCreateUserForm() { + await find.clickByButtonText('Create user'); + } + + async createUser(user: UserFormValues) { + await this.clickElasticsearchUsers(); + await this.clickCreateNewUser(); + await this.fillUserForm(user); + await this.submitCreateUserForm(); + } + async addRole(roleName: string, roleObj: Role) { const self = this; - await this.clickNewRole(); + await this.clickCreateNewRole(); // We have to use non-test-subject selectors because this markup is generated by ui-select. log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); @@ -498,37 +504,23 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const dropdown = await testSubjects.find('rolesDropdown'); const input = await dropdown.findByCssSelector('input'); await input.type(role); - await testSubjects.click(`roleOption-${role}`); + await find.clickByCssSelector(`[role=option][title="${role}"]`); await testSubjects.click('comboBoxToggleListButton'); - await testSubjects.find(`roleOption-${role}`); } - deleteUser(username: string) { - let alertText: string; + async deleteUser(username: string) { log.debug('Delete user ' + username); - return find - .clickByDisplayedLinkText(username) - .then(() => { - return PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - }) - .then(() => { - log.debug('Find delete button and click'); - return testSubjects.click('userFormDeleteButton'); - }) - .then(() => { - return PageObjects.common.sleep(2000); - }) - .then(() => { - return testSubjects.getVisibleText('confirmModalBodyText'); - }) - .then((alert) => { - alertText = alert; - log.debug('Delete user alert text = ' + alertText); - return testSubjects.click('confirmModalConfirmButton'); - }) - .then(() => { - return alertText; - }); + await find.clickByDisplayedLinkText(username); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + + log.debug('Find delete button and click'); + await find.clickByButtonText('Delete user'); + await PageObjects.common.sleep(2000); + + const confirmText = await testSubjects.getVisibleText('confirmModalBodyText'); + log.debug('Delete user alert text = ' + confirmText); + await testSubjects.click('confirmModalConfirmButton'); + return confirmText; } } return new SecurityPage(); diff --git a/yarn.lock b/yarn.lock index 828a3b630a838..e7870415b0dda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5137,10 +5137,10 @@ dependencies: "@types/sizzle" "*" -"@types/js-cookie@2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" - integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== +"@types/js-cookie@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" + integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== "@types/js-search@^1.4.0": version "1.4.0" @@ -6374,10 +6374,10 @@ dependencies: tslib "^1.9.3" -"@xobotyi/scrollbar-width@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb" - integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA== +"@xobotyi/scrollbar-width@1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -13368,7 +13368,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== @@ -23597,24 +23597,30 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-use@^13.27.0: - version "13.27.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa" - integrity sha512-2lyTyqJWyvnaP/woVtDcFS4B5pUYz0FQWI9pVHk/6TBWom2x3/ziJthkEn/LbCA9Twv39xSQU7Dn0zdIWfsNTQ== +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^15.3.4: + version "15.3.4" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.4.tgz#f853d310bd71f75b38900a8caa3db93f6dc6e872" + integrity sha512-cHq1dELW6122oi1+xX7lwNyE/ugZs5L902BuO8eFJCfn2api1KeuPVG1M/GJouVARoUf54S2dYFMKo5nQXdTag== dependencies: - "@types/js-cookie" "2.2.5" - "@xobotyi/scrollbar-width" "1.9.4" + "@types/js-cookie" "2.2.6" + "@xobotyi/scrollbar-width" "1.9.5" copy-to-clipboard "^3.2.0" - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" fast-shallow-equal "^1.0.0" js-cookie "^2.2.1" nano-css "^5.2.1" + react-universal-interface "^0.6.2" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1" throttle-debounce "^2.1.0" ts-easing "^0.2.0" - tslib "^1.10.0" + tslib "^2.0.0" react-virtualized-auto-sizer@^1.0.2: version "1.0.2" From e8f338e78c47969801d6de9f7822f7020eecc34c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 25 Jan 2021 14:56:55 +0100 Subject: [PATCH 30/60] [Uptime] Added View performance breakdown button (#88658) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/common/step_detail_link.tsx | 17 ++++---- .../monitor/synthetics/executed_step.tsx | 42 ++++++++----------- .../monitor/synthetics/translations.ts | 14 +++++++ 3 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/translations.ts diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx index a8e4c90f2d29a..886496a7f6e2f 100644 --- a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -5,8 +5,7 @@ */ import React, { FC } from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link } from 'react-router-dom'; +import { ReactRouterEuiButton } from './react_router_helpers'; interface StepDetailLinkProps { /** @@ -23,10 +22,14 @@ export const StepDetailLink: FC = ({ children, checkGroupId const to = `/journey/${checkGroupId}/step/${stepIndex}`; return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 01a599f8e8a60..934427643757d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -13,6 +13,7 @@ import { StepScreenshotDisplay } from './step_screenshot_display'; import { StatusBadge } from './status_badge'; import { Ping } from '../../../../common/runtime_types'; import { StepDetailLink } from '../../common/step_detail_link'; +import { VIEW_PERFORMANCE } from './translations'; const CODE_BLOCK_OVERFLOW_HEIGHT = 360; @@ -26,24 +27,9 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) return ( <>
-
- {step.synthetics?.step?.index && checkGroup ? ( - - - - - - - - ) : ( - + + + = ({ step, index, checkGroup }) /> - )} -
- -
- -
+ +
+ +
+ +
@@ -73,6 +59,14 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) /> + {step.synthetics?.step?.index && ( + + + {VIEW_PERFORMANCE} + + + + )} Date: Mon, 25 Jan 2021 15:01:29 +0100 Subject: [PATCH 31/60] Ignore missing asset errors on remove. (#89115) --- x-pack/plugins/fleet/server/services/epm/packages/remove.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 331b6bfa882da..94e81e296b5a9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -115,7 +115,10 @@ async function deleteAssets( try { await Promise.all(deletePromises); } catch (err) { - logger.error(err); + // in the rollback case, partial installs are likely, so missing assets are not an error + if (!savedObjectsClient.errors.isNotFoundError(err)) { + logger.error(err); + } } } From 93c46f5dfc88b74cd3b2790588aa457ac55bfedf Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 25 Jan 2021 15:29:10 +0100 Subject: [PATCH 32/60] Remove tag name validation (#88800) * Remove tag name validation * remove i18n key * add FTR test on searching for tag with special chars in name --- docs/management/managing-tags.asciidoc | 3 +- .../common/validation.test.ts | 6 +- .../common/validation.ts | 6 -- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../global_search/search_syntax/data.json | 56 +++++++++++++++++++ .../global_search/global_search_bar.ts | 13 ++++- .../tagging_api/apis/create.ts | 4 +- .../tagging_api/apis/update.ts | 4 +- .../functional/tests/create.ts | 4 +- .../functional/tests/edit.ts | 4 +- 11 files changed, 79 insertions(+), 23 deletions(-) diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc index 3da98b2281fdc..88fdef66a7418 100644 --- a/docs/management/managing-tags.asciidoc +++ b/docs/management/managing-tags.asciidoc @@ -37,8 +37,7 @@ Create a tag to assign to your saved objects. image::images/tags/create-tag.png[Tag creation popin] . Enter a name and select a color for the new tag. + -The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character, -and cannot be longer than 50 characters. +The name cannot be longer than 50 characters. . Click *Create tag*. [float] diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.test.ts b/x-pack/plugins/saved_objects_tagging/common/validation.test.ts index 232387e964cbf..a601a96a49e75 100644 --- a/x-pack/plugins/saved_objects_tagging/common/validation.test.ts +++ b/x-pack/plugins/saved_objects_tagging/common/validation.test.ts @@ -20,10 +20,8 @@ describe('Tag attributes validation', () => { ); }); - it('returns an error message if the name contains invalid characters', () => { - expect(validateTagName('t^ag+name&')).toMatchInlineSnapshot( - `"Tag name can only include a-z, 0-9, _, -,:."` - ); + it('does not return an error message if the name contains special characters', () => { + expect(validateTagName('t^ag+name&')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.ts b/x-pack/plugins/saved_objects_tagging/common/validation.ts index 12149d7bdbe79..5cb9e068516fe 100644 --- a/x-pack/plugins/saved_objects_tagging/common/validation.ts +++ b/x-pack/plugins/saved_objects_tagging/common/validation.ts @@ -12,7 +12,6 @@ export const tagNameMaxLength = 50; export const tagDescriptionMaxLength = 100; const hexColorRegexp = /^#[0-9A-F]{6}$/i; -const nameValidCharsRegexp = /^[0-9A-Z:\-_\s]+$/i; export interface TagValidation { valid: boolean; @@ -49,11 +48,6 @@ export const validateTagName = (name: string): string | undefined => { }, }); } - if (!nameValidCharsRegexp.test(name)) { - return i18n.translate('xpack.savedObjectsTagging.validation.name.errorInvalidCharacters', { - defaultMessage: 'Tag name can only include a-z, 0-9, _, -,:.', - }); - } }; export const validateTagDescription = (description: string): string | undefined => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3a579adcf88c2..ef2149c4931fa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16633,7 +16633,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "タグ", "xpack.savedObjectsTagging.validation.color.errorInvalid": "タグ色は有効な 16 進数値色でなければなりません", "xpack.savedObjectsTagging.validation.description.errorTooLong": "タグ説明は {length} 文字以下で入力してください", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "タグ名には、a-z、0-9、-、: のみを使用できます。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "タグ名は {length} 文字以下で入力してください", "xpack.savedObjectsTagging.validation.name.errorTooShort": "タグ名は {length} 文字以上で入力してください", "xpack.searchProfiler.advanceTimeDescription": "イテレーターを次のドキュメントに進めるためにかかった時間。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9099fa0267ba9..08d064ce8a05c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16675,7 +16675,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "标签", "xpack.savedObjectsTagging.validation.color.errorInvalid": "标签颜色必须为有效的十六进制颜色", "xpack.savedObjectsTagging.validation.description.errorTooLong": "标签描述不能超过 {length} 个字符。", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "标签名称只能包含 a-z、0-9、_、-、:。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "标签名称不能超过 {length} 个字符", "xpack.savedObjectsTagging.validation.name.errorTooShort": "标签名称必须至少有 {length} 个字符", "xpack.searchProfiler.advanceTimeDescription": "将迭代器推进至下一文档所用时间。", diff --git a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json index 69220756639dc..8379290f5d9bb 100644 --- a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json +++ b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json @@ -88,6 +88,24 @@ } } +{ + "type": "doc", + "value": { + "id": "tag:tag-special-chars", + "index": ".kibana", + "source": { + "tag": { + "name": "my%tag", + "description": "Special chars", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + { "type": "doc", "value": { @@ -356,3 +374,41 @@ } } } + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-special-chars", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 4 (tag-special-chars)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-special-chars", + "name": "tag-special-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + + diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index f0c70ee8f718d..6f84440fc27e6 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('GlobalSearchBar', function () { + describe('TOTO GlobalSearchBar', function () { const { common, navigationalSearch } = getPageObjects(['common', 'navigationalSearch']); const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -61,6 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); it('shows a suggestion when searching for a term matching a tag name', async () => { @@ -94,6 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -111,6 +113,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -181,6 +184,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results.map((result) => result.label)).to.eql(['My awesome vis (tag-4)']); }); + it('allows to filter by tags containing special characters', async () => { + await navigationalSearch.searchFor('tag:"my%tag"'); + + const results = await navigationalSearch.getDisplayedResults(); + + expect(results.map((result) => result.label)).to.eql(['dashboard 4 (tag-special-chars)']); + }); + it('returns no results when searching for an unknown tag', async () => { await navigationalSearch.searchFor('tag:unknown'); diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts index bd7fa7538703c..30008e635b628 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/create`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts index 7b4298607c666..ddf39ccf90b34 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/tag-1`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -92,7 +92,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/create.ts b/x-pack/test/saved_object_tagging/functional/tests/create.ts index b62e9a70b43e8..2f2db856c0657 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/create.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/create.ts @@ -54,7 +54,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, @@ -73,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/edit.ts b/x-pack/test/saved_object_tagging/functional/tests/edit.ts index 1883d3f23dc9d..1de101433179d 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/edit.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/edit.ts @@ -71,7 +71,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', }, { submit: true } ); @@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'edited description', color: '#FF00CC', }, From f4c43002017a93ad77c4b8c2abe4bc5ee6d1cded Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 25 Jan 2021 15:35:30 +0100 Subject: [PATCH 33/60] [Discover] Deangularize $element and $timeout (#88214) * Remove $element for document.getElementById * Remove $timeout --- .../public/application/angular/discover.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 75fed9f809aa6..946baa7f4ecb1 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -175,7 +175,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($element, $route, $scope, $timeout, Promise) { +function discoverController($route, $scope, Promise) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -725,20 +725,20 @@ function discoverController($element, $route, $scope, $timeout, Promise) { $route.reload(); }; - $scope.onSkipBottomButtonClick = function () { + $scope.onSkipBottomButtonClick = async () => { // show all the Rows $scope.minimumVisibleRows = $scope.hits; // delay scrolling to after the rows have been rendered - const bottomMarker = $element.find('#discoverBottomMarker'); - $timeout(() => { - bottomMarker.focus(); - // The anchor tag is not technically empty (it's a hack to make Safari scroll) - // so the browser will show a highlight: remove the focus once scrolled - $timeout(() => { - bottomMarker.blur(); - }, 0); - }, 0); + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker.focus(); + await wait(50); + bottomMarker.blur(); }; $scope.newQuery = function () { From eed938396949f9ca2127c2859eded50781b3f14b Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 25 Jan 2021 15:36:48 +0100 Subject: [PATCH 34/60] Migrations v2 docs (#88820) * Migrations v2 docs * Not all kibana distributions automatically restarted a killed process * Mention that we add a write block to the outdated index * Formating: collapse three notes into a single note with three bullet points * Update docs/setup/upgrade/upgrade-standard.asciidoc Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> * Add table of outdated / upgraded indices per version of Kibana * Review feedback: separate section for multi-instance upgrade migrations * Review feedback: link to saved objects management * Review feedback: stronger wording for not deleting any indices Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> --- .../setup/upgrade/upgrade-migrations.asciidoc | 79 +++++++++---------- docs/setup/upgrade/upgrade-standard.asciidoc | 14 ++-- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 74d097164c4a7..7436536d22781 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,11 +1,13 @@ [[upgrade-migrations]] === Upgrade migrations -Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off. +Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding. + WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. [float] @@ -14,19 +16,35 @@ WARNING: The following instructions assumes {kib} is using the default index nam Saved objects are stored in two indices: -* `.kibana_N`, or if set, the `kibana.index` configuration setting -* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting +* `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. +* `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. -For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index. +The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices. + +The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. + +.Saved object indices and aliases per {kib} version +[options="header"] +[cols="a,a,a"] +|======================= +|Upgrading from version | Outdated index (alias) | Upgraded index (alias) +| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001` +(`.kibana` alias) + +`.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) +| 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) -While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required. +`.kibana_task_manager_N` (`.kibana_task_manager` alias) +|======================= -When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index. +==== Upgrading multiple {kib} instances +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. -Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic. +Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. -NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. + -Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias. +For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] @@ -54,50 +72,31 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu * a "green" cluster status [float] -===== Running different versions of {kib} connected to the same {es} index -Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss. +===== Different versions of {kib} connected to the same {es} index +When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins. [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. +For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. [float] [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade. - -As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations. - -However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted. - -[float] -===== Retry a migration by restoring a backup snapshot: - -1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. -2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -3. Delete all saved object indices with `DELETE /.kibana*` -4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] -5. Start up all {kib} instances to retry the upgrade migration. - -[float] -===== (Not recommended) Retry a migration without a backup snapshot: +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. -1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices. -3. Remove any migration locks e.g. `DELETE /.kibana_5`. -4. Start up all {kib} instances. +If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause. +If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. [float] ===== Rollback by restoring a backup snapshot: @@ -111,17 +110,15 @@ In order to rollback after a failed upgrade migration, the saved object indices [float] ===== (Not recommended) Rollback without a backup snapshot: -WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss. - 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. Create a backup snapshot of the `.kibana*` indices. -3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs. -4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2` -5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. [float] [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index b27bb8867e624..b43da6aef9765 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -15,17 +15,17 @@ necessary remediation steps as per those instructions. [float] ==== Upgrading multiple {kib} instances -WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. +NOTE: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. -Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. - -The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic. +Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause data loss. This is because older instances will continue to write saved objects in a different format than the newer instances. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. [float] ==== Upgrade using a `deb` or `rpm` package . Stop the existing {kib} process using the appropriate command for your - system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. + system. If you have multiple {kib} instances connecting to the same {es} + cluster ensure that all instances are stopped before proceeding to the next + step to avoid data loss. . Use `rpm` or `dpkg` to install the new package. All files should be placed in their proper locations and config files should not be overwritten. + @@ -65,5 +65,7 @@ and becomes a new instance in the monitoring data. . Install the appropriate versions of all your plugins for your new installation using the `kibana-plugin` script. Check out the <> documentation for more information. -. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. +. Stop the old {kib} process. If you have multiple {kib} instances connecting + to the same {es} cluster ensure that all instances are stopped before + proceeding to the next step to avoid data loss. . Start the new {kib} process. From 164d6b2d9934927373b1ad01246613674c05accf Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 25 Jan 2021 15:51:11 +0100 Subject: [PATCH 35/60] [discover] add valye formatter on y axis and display only integer values(#88941) --- .../public/application/angular/directives/histogram.tsx | 6 ++++++ .../public/application/angular/helpers/point_series.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index ff10feea46d47..b12de3f4496c5 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -154,6 +154,10 @@ export class DiscoverHistogram extends Component xAxisFormatter.convert(value)} /> ; xAxisOrderedValues: number[]; xAxisFormat: Dimension['format']; + yAxisFormat: Dimension['format']; xAxisLabel: Column['name']; yAxisLabel?: Column['name']; ordered: Ordered; @@ -76,7 +77,7 @@ export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); chart.xAxisFormat = x.format; chart.xAxisLabel = table.columns[x.accessor].name; - + chart.yAxisFormat = y.format; const { intervalESUnit, intervalESValue, interval, bounds } = x.params; chart.ordered = { date: true, From e7e42a47117da9ad2415f0108736842dd3798a8c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 25 Jan 2021 16:07:53 +0100 Subject: [PATCH 36/60] [Uptime] Display networks requests total (#88672) --- .../common/runtime_types/network_events.ts | 1 + .../waterfall/waterfall_chart_container.tsx | 5 +- .../waterfall/waterfall_chart_wrapper.tsx | 5 +- .../network_requests_total.test.tsx | 28 ++++++++++ .../components/network_requests_total.tsx | 44 ++++++++++++++++ .../synthetics/waterfall/components/styles.ts | 7 ++- .../waterfall/components/waterfall_chart.tsx | 16 +++++- .../waterfall/context/waterfall_chart.tsx | 17 +++++- .../public/state/reducers/network_events.ts | 12 ++++- .../lib/requests/get_network_events.test.ts | 52 +++++++++++-------- .../server/lib/requests/get_network_events.ts | 45 +++++++++------- .../network_events/get_network_events.ts | 4 +- 12 files changed, 183 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index 6104758f28fd8..fc666c803e2c3 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -41,6 +41,7 @@ export type NetworkEvent = t.TypeOf; export const SyntheticsNetworkEventsApiResponseType = t.type({ events: t.array(NetworkEventType), + total: t.number, }); export type SyntheticsNetworkEventsApiResponse = t.TypeOf< diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx index 7657ca7f9c64a..680e3f257841e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -59,7 +59,10 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex )} {networkEvents && !networkEvents.loading && networkEvents.events.length > 0 && ( - + )} ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index a84765c4ea154..7b904511b58ab 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -48,10 +48,11 @@ export const renderLegendItem: RenderItem = (item) => { }; interface Props { + total: number; data: NetworkItems; } -export const WaterfallChartWrapper: React.FC = ({ data }) => { +export const WaterfallChartWrapper: React.FC = ({ data, total }) => { const [networkData] = useState(data); const { series, domain } = useMemo(() => { @@ -66,6 +67,8 @@ export const WaterfallChartWrapper: React.FC = ({ data }) => { return ( { + it('message in case total is greater than fetched', () => { + const { getByText, getByLabelText } = render( + + ); + + expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); + expect(getByLabelText('Info')).toBeInTheDocument(); + }); + + it('message in case total is equal to fetched requests', () => { + const { getByText } = render( + + ); + + expect(getByText('500 network requests')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx new file mode 100644 index 0000000000000..c54e32238f81c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; +import { NetworkRequestsTotalStyle } from './styles'; + +interface Props { + totalNetworkRequests: number; + fetchedNetworkRequests: number; +} + +export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { + return ( + + + {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { + defaultMessage: '{numNetworkRequests} network requests', + values: { + numNetworkRequests: + totalNetworkRequests > fetchedNetworkRequests + ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { + defaultMessage: 'First {count}', + values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, + }) + : totalNetworkRequests, + }, + })} + + {totalNetworkRequests > fetchedNetworkRequests && ( + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 1f70354db154e..7bf5100730f5e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; import { euiStyled } from '../../../../../../../observability/public'; import { FIXED_AXIS_HEIGHT } from './constants'; @@ -103,3 +103,8 @@ export const WaterfallChartTooltip = euiStyled.div` color: ${(props) => props.theme.eui.euiColorLightestShade}; padding: ${(props) => props.theme.eui.paddingSizes.s}; `; + +export const NetworkRequestsTotalStyle = euiStyled(EuiText)` + line-height: ${FIXED_AXIS_HEIGHT}px; + margin-left: ${(props) => props.theme.eui.paddingSizes.m} +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index e937c3d35ec08..e449fed6decf4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -37,6 +37,7 @@ import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { NetworkRequestsTotal } from './network_requests_total'; const Tooltip = (tooltipInfo: TooltipInfo) => { const { data, renderTooltipItem } = useWaterfallContext(); @@ -84,7 +85,13 @@ export const WaterfallChart = ({ maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { - const { data, sidebarItems, legendItems } = useWaterfallContext(); + const { + data, + sidebarItems, + legendItems, + totalNetworkRequests, + fetchedNetworkRequests, + } = useWaterfallContext(); const [darkMode] = useUiSetting$('theme:darkMode'); @@ -115,7 +122,12 @@ export const WaterfallChart = ({ {shouldRenderSidebar && ( - + + + )} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index ccee9d7994c80..4cf22f317bbd4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -8,6 +8,8 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; export interface IWaterfallContext { + totalNetworkRequests: number; + fetchedNetworkRequests: number; data: WaterfallData; sidebarItems?: unknown[]; legendItems?: unknown[]; @@ -20,6 +22,8 @@ export interface IWaterfallContext { export const WaterfallContext = createContext>({}); interface ProviderProps { + totalNetworkRequests: number; + fetchedNetworkRequests: number; data: IWaterfallContext['data']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; @@ -32,9 +36,20 @@ export const WaterfallProvider: React.FC = ({ sidebarItems, legendItems, renderTooltipItem, + totalNetworkRequests, + fetchedNetworkRequests, }) => { return ( - + {children} ); diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts index 44a23b0fa53d7..666617f785182 100644 --- a/x-pack/plugins/uptime/public/state/reducers/network_events.ts +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -18,6 +18,7 @@ export interface NetworkEventsState { [checkGroup: string]: { [stepIndex: number]: { events: NetworkEvent[]; + total: number; loading: boolean; error?: Error; }; @@ -45,16 +46,19 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: true, events: [], + total: 0, } : { loading: true, events: [], + total: 0, }, } : { [stepIndex]: { loading: true, events: [], + total: 0, }, }, }), @@ -62,7 +66,7 @@ export const networkEventsReducer = handleActions( [String(getNetworkEventsSuccess)]: ( state: NetworkEventsState, { - payload: { events, checkGroup, stepIndex }, + payload: { events, total, checkGroup, stepIndex }, }: Action ) => { return { @@ -74,16 +78,19 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: false, events, + total, } : { loading: false, events, + total, }, } : { [stepIndex]: { loading: false, events, + total, }, }, }; @@ -101,11 +108,13 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: false, events: [], + total: 0, error, } : { loading: false, events: [], + total: 0, error, }, } @@ -113,6 +122,7 @@ export const networkEventsReducer = handleActions( [stepIndex]: { loading: false, events: [], + total: 0, error, }, }, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index e8618fabc4cca..2d590e80ca42d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -158,6 +158,7 @@ describe('getNetworkEvents', () => { esClient.search.mockResolvedValueOnce({ body: { hits: { + total: { value: 1 }, hits: mockHits, }, }, @@ -196,6 +197,7 @@ describe('getNetworkEvents', () => { }, }, "size": 1000, + "track_total_hits": true, }, "index": "heartbeat-8*", }, @@ -210,6 +212,7 @@ describe('getNetworkEvents', () => { esClient.search.mockResolvedValueOnce({ body: { hits: { + total: { value: 1 }, hits: mockHits, }, }, @@ -222,30 +225,33 @@ describe('getNetworkEvents', () => { }); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "loadEndTime": 3287298.251, - "method": "GET", - "mimeType": "image/gif", - "requestSentTime": 3287154.973, - "requestStartTime": 3287155.502, - "status": 200, - "timestamp": "2020-12-14T10:46:39.183Z", - "timings": Object { - "blocked": 0.21400000014182297, - "connect": -1, - "dns": -1, - "proxy": -1, - "queueing": 0.5289999999149586, - "receive": 0.5340000002433953, - "send": 0.18799999998009298, - "ssl": -1, - "total": 143.27800000000934, - "wait": 141.81299999972907, + Object { + "events": Array [ + Object { + "loadEndTime": 3287298.251, + "method": "GET", + "mimeType": "image/gif", + "requestSentTime": 3287154.973, + "requestStartTime": 3287155.502, + "status": 200, + "timestamp": "2020-12-14T10:46:39.183Z", + "timings": Object { + "blocked": 0.21400000014182297, + "connect": -1, + "dns": -1, + "proxy": -1, + "queueing": 0.5289999999149586, + "receive": 0.5340000002433953, + "send": 0.18799999998009298, + "ssl": -1, + "total": 143.27800000000934, + "wait": 141.81299999972907, + }, + "url": "www.test.com", }, - "url": "www.test.com", - }, - ] + ], + "total": 1, + } `); }); }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 1353175a8f94d..ec1fffd62350d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -14,9 +14,10 @@ interface GetNetworkEventsParams { export const getNetworkEvents: UMElasticsearchQueryFn< GetNetworkEventsParams, - NetworkEvent[] + { events: NetworkEvent[]; total: number } > = async ({ uptimeEsClient, checkGroup, stepIndex }) => { const params = { + track_total_hits: true, query: { bool: { filter: [ @@ -36,24 +37,28 @@ export const getNetworkEvents: UMElasticsearchQueryFn< const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); - return result.hits.hits.map((event: any) => { - const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); - const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); - const requestStartTime = - event._source.synthetics.payload.response && event._source.synthetics.payload.response.timing - ? microToMillis(event._source.synthetics.payload.response.timing.request_time) - : undefined; + return { + total: result.hits.total.value, + events: result.hits.hits.map((event: any) => { + const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); + const requestStartTime = + event._source.synthetics.payload.response && + event._source.synthetics.payload.response.timing + ? microToMillis(event._source.synthetics.payload.response.timing.request_time) + : undefined; - return { - timestamp: event._source['@timestamp'], - method: event._source.synthetics.payload?.method, - url: event._source.synthetics.payload?.url, - status: event._source.synthetics.payload?.status, - mimeType: event._source.synthetics.payload?.response?.mime_type, - requestSentTime, - requestStartTime, - loadEndTime, - timings: event._source.synthetics.payload.timings, - }; - }); + return { + timestamp: event._source['@timestamp'], + method: event._source.synthetics.payload?.method, + url: event._source.synthetics.payload?.url, + status: event._source.synthetics.payload?.status, + mimeType: event._source.synthetics.payload?.response?.mime_type, + requestSentTime, + requestStartTime, + loadEndTime, + timings: event._source.synthetics.payload.timings, + }; + }), + }; }; diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts index f24b319baff00..7a6355ea4247d 100644 --- a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts +++ b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts @@ -26,8 +26,6 @@ export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLi stepIndex, }); - return { - events: result, - }; + return result; }, }); From 1714b22de72bd63000649516b4c0cd4068ea00f3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 25 Jan 2021 17:41:25 +0200 Subject: [PATCH 37/60] [Security Solution][Case] Improve cases and actions docs (#87817) --- x-pack/plugins/actions/README.md | 179 +++++++++++++++++++------------ x-pack/plugins/case/README.md | 40 ++++--- 2 files changed, 135 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 12c3ab12a6998..9472cbf400a6a 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -69,21 +69,26 @@ Table of Contents - [`secrets`](#secrets-6) - [`params`](#params-6) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) - - [`subActionParams (getFields)`](#subactionparams-getfields-1) + - [`subActionParams (getFields)`](#subactionparams-getfields) - [Jira](#jira) - [`config`](#config-7) - [`secrets`](#secrets-7) - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) + - [`subActionParams (getIncident)`](#subactionparams-getincident) - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) - - [`subActionParams (getFields)`](#subactionparams-getfields-2) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype) + - [`subActionParams (issues)`](#subactionparams-issues) + - [`subActionParams (issue)`](#subactionparams-issue) + - [`subActionParams (getFields)`](#subactionparams-getfields-1) - [IBM Resilient](#ibm-resilient) - [`config`](#config-8) - [`secrets`](#secrets-8) - [`params`](#params-8) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - - [`subActionParams (getFields)`](#subactionparams-getfields-3) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (getFields)`](#subactionparams-getfields-2) + - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) + - [`subActionParams (severity)`](#subactionparams-severity) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -526,17 +531,17 @@ The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com ### `params` -| Property | Description | Type | -| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | -| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | -| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | -| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | -| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | -| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | -| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | -| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | -| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | +| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | +| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | +| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | +| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | +| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | +| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | +| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | +| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2). @@ -550,9 +555,9 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `config` -| Property | Description | Type | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -| apiUrl | ServiceNow instance URL. | string | +| Property | Description | Type | +| -------- | ------------------------ | ------ | +| apiUrl | ServiceNow instance URL. | string | ### `secrets` @@ -563,24 +568,28 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | --------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `getFields`, and `pushToService` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` -| Property | Description | Type | -| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object. | string | -| title | The title of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | -| comment | A comment. | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | -| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | -| severity | The name of the severity in ServiceNow. | string _(optional)_ | -| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | -| impact | The name of the impact in ServiceNow. | string _(optional)_ | +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The ServiceNow incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | +| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| severity | The name of the severity in ServiceNow. | string _(optional)_ | +| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | +| impact | The name of the impact in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -596,9 +605,9 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `config` -| Property | Description | Type | -| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| apiUrl | Jira instance URL. | string | +| Property | Description | Type | +| -------- | ------------------ | ------ | +| apiUrl | Jira instance URL. | string | ### `secrets` @@ -609,48 +618,71 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `params` -| Property | Description | Type | -| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, and `getFields` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object | string | -| title | The title of the issue | string _(optional)_ | -| description | The description of the issue | string _(optional)_ | -| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | -| issueType | The id of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | -| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Jira incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | -#### `subActionParams (issueTypes)` +The following table describes the properties of the `incident` object. -No parameters for `issueTypes` sub-action. Provide an empty object `{}`. +| Property | Description | Type | +| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue | string | +| description | The description of the issue | string _(optional)_ | +| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| issueType | The id of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. | string[] _(optional)_ | +| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | -#### `subActionParams (getFields)` +#### `subActionParams (getIncident)` -No parameters for `getFields` sub-action. Provide an empty object `{}`. +| Property | Description | Type | +| ---------- | --------------------------- | ------ | +| externalId | The id of the issue in Jira | string | -#### `subActionParams (pushToService)` +#### `subActionParams (issueTypes)` + +No parameters for `issueTypes` sub-action. Provide an empty object `{}`. + +#### `subActionParams (fieldsByIssueType)` | Property | Description | Type | | -------- | -------------------------------- | ------ | | id | The id of the issue type in Jira | string | +#### `subActionParams (issues)` + +| Property | Description | Type | +| -------- | ----------------------- | ------ | +| title | The title to search for | string | + +#### `subActionParams (issue)` + +| Property | Description | Type | +| -------- | --------------------------- | ------ | +| id | The id of the issue in Jira | string | + +#### `subActionParams (getFields)` + +No parameters for `getFields` sub-action. Provide an empty object `{}`. + ## IBM Resilient ID: `.resilient` ### `config` -| Property | Description | Type | -| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | IBM Resilient instance URL. | string | +| Property | Description | Type | +| -------- | --------------------------- | ------ | +| apiUrl | IBM Resilient instance URL. | string | ### `secrets` @@ -661,19 +693,24 @@ ID: `.resilient` ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The IBM Resilient incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + | Property | Description | Type | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object | string | -| title | The title of the incident | string _(optional)_ | +| name | The title of the incident | string _(optional)_ | | description | The description of the incident | string _(optional)_ | -| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | | externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | | severityCode | IBM Resilient id of the severity code. | number _(optional)_ | @@ -682,6 +719,14 @@ ID: `.resilient` No parameters for `getFields` sub-action. Provide an empty object `{}`. +#### `subActionParams (incidentTypes)` + +No parameters for `incidentTypes` sub-action. Provide an empty object `{}`. + +#### `subActionParams (severity)` + +No parameters for `severity` sub-action. Provide an empty object `{}`. + # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md index 30011148cd1e7..069441ab640ee 100644 --- a/x-pack/plugins/case/README.md +++ b/x-pack/plugins/case/README.md @@ -4,8 +4,7 @@ Elastic is developing a Case Management Workflow. Follow our progress: -- [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest) -- [Github Meta](https://github.com/elastic/kibana/issues/50103) +- [Case API Documentation](https://www.elastic.co/guide/en/security/master/cases-overview.html) # Action types @@ -42,27 +41,28 @@ This action type has no `secrets` properties. | description | The case’s description. | string | | tags | String array containing words and phrases that help categorize cases. | string[] | | connector | Object containing the connector’s configuration. | [connector](#connector) | +| settings | Object containing the case’s settings. | [settings](#settings) | #### `subActionParams (update)` -| Property | Description | Type | -| ----------- | ---------------------------------------------------------- | ----------------------- | -| id | The ID of the case being updated. | string | -| tile | The updated case title. | string | -| description | The updated case description. | string | -| tags | The updated case tags. | string | -| connector | Object containing the connector’s configuration. | [connector](#connector) | -| status | The updated case status, which can be: `open` or `closed`. | string | -| version | The current case version. | string | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------- | ----------------------- | +| id | The ID of the case being updated. | string | +| tile | The updated case title. | string | +| description | The updated case description. | string | +| tags | The updated case tags. | string | +| connector | Object containing the connector’s configuration. | [connector](#connector) | +| status | The updated case status, which can be: `open`, `in-progress` or `closed`. | string | +| settings | Object containing the case’s settings. | [settings](#settings) | +| version | The current case version. | string | #### `subActionParams (addComment)` -| Property | Description | Type | -| -------- | ----------------------------------------------------------------------- | ----------------- | -| type | The type of the comment | `user` \| `alert` | -| comment | The comment. Valid only when type is `user`. | string | -| alertId | The alert ID. Valid only when the type is `alert` | string | -| index | The index where the alert is saved. Valid only when the type is `alert` | string | +| Property | Description | Type | +| -------- | ------------------------ | ------ | +| type | The type of the comment. | `user` | +| comment | The comment. | string | + #### `connector` | Property | Description | Type | @@ -96,3 +96,9 @@ For IBM Resilient connectors: | ------------ | ------------------------------- | -------- | | issueTypes | The issue types of the issue. | string[] | | severityCode | The severity code of the issue. | string | + +#### `settings` + +| Property | Description | Type | +| ---------- | ------------------------------ | ------- | +| syncAlerts | Turn on or off alert synching. | boolean | \ No newline at end of file From 207c8eac5c50b45212b4c34e781fe2fef90fbc44 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 25 Jan 2021 15:56:29 +0000 Subject: [PATCH 38/60] corrected terminology in PR template (#89095) We recently added a usage of `whitelist`, I've changed it to `allowlist` --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index efba93350b8fb..2a5fc914662b6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) -- [ ] If a plugin configuration key changed, check if it needs to be whitelisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) +- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) From f6837a1f66db9909768afdd68bf2f0be8c3087f4 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 25 Jan 2021 15:57:50 +0000 Subject: [PATCH 39/60] made unit test more reliable (#89094) Made unit test more reliable by using resolving promises rather than timed `await`s that could be flaky when the node event loop is overwhelmed. --- .../task_manager/server/task_pool.test.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 324e376c32d95..14ad0561928f8 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -13,6 +13,7 @@ import { Logger } from '../../../../src/core/server'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; import moment from 'moment'; +import uuid from 'uuid'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -133,7 +134,7 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); expect(logger.debug).toHaveBeenCalledWith( - 'Task TaskType "shooooo" failed in attempt to run: Saved object [task/foo] not found' + `Task TaskType "shooooo" failed in attempt to run: Saved object [task/${taskFailedToRun.id}] not found` ); expect(logger.warn).not.toHaveBeenCalled(); @@ -203,26 +204,28 @@ describe('TaskPool', () => { sinon.assert.calledOnce(secondRun); }); - test.skip('run cancels expired tasks prior to running new tasks', async () => { + test('run cancels expired tasks prior to running new tasks', async () => { const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(2), logger, }); - const readyToExpire = resolvable(); + const haltUntilWeAfterFirstRun = resolvable(); const taskHasExpired = resolvable(); + const haltTaskSoThatItCanBeCanceled = resolvable(); + const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); const now = new Date(); const result = await pool.run([ { - ...mockTask(), + ...mockTask({ id: '1' }), async run() { - await readyToExpire; + await haltUntilWeAfterFirstRun; this.isExpired = true; taskHasExpired.resolve(); - await sleep(10); + await haltTaskSoThatItCanBeCanceled; return asOk({ state: {} }); }, get expiration() { @@ -235,9 +238,10 @@ describe('TaskPool', () => { cancel: shouldRun, }, { - ...mockTask(), + ...mockTask({ id: '2' }), async run() { - await sleep(10); + // halt here so that we can verify that this task is counted in `occupiedWorkers` + await haltUntilWeAfterFirstRun; return asOk({ state: {} }); }, cancel: shouldNotRun, @@ -248,16 +252,19 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); - readyToExpire.resolve(); + // release first stage in task so that it has time to expire, but not complete + haltUntilWeAfterFirstRun.resolve(); await taskHasExpired; - expect(await pool.run([{ ...mockTask() }])).toBeTruthy(); + expect(await pool.run([{ ...mockTask({ id: '3' }) }])).toBeTruthy(); sinon.assert.calledOnce(shouldRun); sinon.assert.notCalled(shouldNotRun); - expect(pool.occupiedWorkers).toEqual(2); - expect(pool.availableWorkers).toEqual(0); + expect(pool.occupiedWorkers).toEqual(1); + expect(pool.availableWorkers).toEqual(1); + + haltTaskSoThatItCanBeCanceled.resolve(); expect(logger.warn).toHaveBeenCalledWith( `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).` @@ -355,10 +362,10 @@ describe('TaskPool', () => { }); } - function mockTask() { + function mockTask(overrides = {}) { return { isExpired: false, - id: 'foo', + id: uuid.v4(), cancel: async () => undefined, markTaskAsRunning: jest.fn(async () => true), run: mockRun(), @@ -377,6 +384,7 @@ describe('TaskPool', () => { createTaskRunner: jest.fn(), }; }, + ...overrides, }; } }); From e251ff4be593d45d6094efb414b1dccecff42072 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 25 Jan 2021 08:05:32 -0800 Subject: [PATCH 40/60] [APM] Renames significant terms feature to "Correlations" (#88974) (#89028) * [APM] Renames significant terms feature to "Correlations" (#88974) * fix capitalizations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/common/ui_settings_keys.ts | 2 +- .../app/Correlations/LatencyCorrelations.tsx | 2 +- .../apm/public/components/app/Correlations/index.tsx | 10 +++++----- x-pack/plugins/apm/server/ui_settings.ts | 9 ++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index ffc2a2ef21fe9..38922fa445a47 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const enableSignificantTerms = 'apm:enableSignificantTerms'; +export const enableCorrelations = 'apm:enableCorrelations'; export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx index b2d88c4c3849b..438303110fbc4 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -128,7 +128,7 @@ export function LatencyCorrelations() { - View significant terms + View correlations @@ -62,7 +62,7 @@ export function Correlations() { > -

Significant terms

+

Correlations

@@ -88,7 +88,7 @@ export function Correlations() { iconType="alert" >

- Significant terms is an experimental feature and in active + Correlations is an experimental feature and in active development. Bugs and surprises are to be expected but let us know your feedback so we can improve it.

diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index c86fb636b5a1a..e9bb747280fc7 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { - enableSignificantTerms, + enableCorrelations, enableServiceOverview, } from '../common/ui_settings_keys'; @@ -16,17 +16,16 @@ import { * uiSettings definitions for APM. */ export const uiSettings: Record> = { - [enableSignificantTerms]: { + [enableCorrelations]: { category: ['observability'], name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { - defaultMessage: 'APM Significant terms (Platinum required)', + defaultMessage: 'APM correlations (Platinum required)', }), value: false, description: i18n.translate( 'xpack.apm.enableCorrelationsExperimentDescription', { - defaultMessage: - 'Enable the experimental Significant terms feature in APM', + defaultMessage: 'Enable the experimental correlations feature in APM', } ), schema: schema.boolean(), From 6391ef9c45409cdd47d13f5b5fb420d092562b68 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 25 Jan 2021 17:08:31 +0100 Subject: [PATCH 41/60] [Observability] Lazy load shared components (#88802) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app/RumDashboard/UXMetrics/index.tsx | 23 ++++++++++------ .../public/components/app/header/index.tsx | 2 +- .../components/app/section/ux/index.tsx | 2 +- .../shared/core_web_vitals/index.tsx | 17 ++++-------- .../components/shared/header_menu_portal.tsx | 12 +++------ .../public/components/shared/index.tsx | 26 +++++++++++++++++++ .../public/components/shared/types.ts | 23 ++++++++++++++++ x-pack/plugins/observability/public/index.ts | 6 ++--- 8 files changed, 78 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/index.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/types.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 392b42cba12e5..29d5750231762 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +17,7 @@ import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './KeyUXMetrics'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; -import { CoreVitals } from '../../../../../../observability/public'; +import { getCoreVitalsComponent } from '../../../../../../observability/public'; import { CsmSharedContext } from '../CsmSharedContext'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; @@ -48,6 +48,18 @@ export function UXMetrics() { sharedData: { totalPageViews }, } = useContext(CsmSharedContext); + const CoreVitals = useMemo( + () => + getCoreVitalsComponent({ + data, + totalPageViews, + loading: status !== 'success', + displayTrafficMetric: true, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [status] + ); + return ( @@ -67,12 +79,7 @@ export function UXMetrics() { - + {CoreVitals} diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index b195bb52e7ed2..097871fe020e5 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { usePluginContext } from '../../../hooks/use_plugin_context'; -import { HeaderMenuPortal } from '../../shared/header_menu_portal'; +import HeaderMenuPortal from '../../shared/header_menu_portal'; const Container = styled.div<{ color: string }>` background: ${(props) => props.color}; diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 43f1072d06fc2..7074a895d058b 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -12,7 +12,7 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { UXHasDataResponse } from '../../../../typings'; -import { CoreVitals } from '../../../shared/core_web_vitals'; +import CoreVitals from '../../../shared/core_web_vitals'; interface Props { bucketSize: string; diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx index f573c8cfc1f97..7d40ce089cec4 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx @@ -16,6 +16,7 @@ import { import { CoreVitalItem } from './core_vital_item'; import { WebCoreVitalsTitle } from './web_core_vitals_title'; import { ServiceName } from './service_name'; +import { CoreVitalProps } from '../types'; export interface UXMetrics { cls: number | null; @@ -29,7 +30,7 @@ export interface UXMetrics { clsRanks: number[]; } -export function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string { +function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string { const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1); if (valueInMs < 1000) { @@ -51,23 +52,15 @@ const CoreVitalsThresholds = { CLS: { good: '0.1', bad: '0.25' }, }; -interface Props { - loading: boolean; - data?: UXMetrics | null; - displayServiceName?: boolean; - serviceName?: string; - totalPageViews?: number; - displayTrafficMetric?: boolean; -} - -export function CoreVitals({ +// eslint-disable-next-line import/no-default-export +export default function CoreVitals({ data, loading, displayServiceName, serviceName, totalPageViews, displayTrafficMetric = false, -}: Props) { +}: CoreVitalProps) { const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {}; return ( diff --git a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx index ca03eb6ddb45a..e209e830d0f37 100644 --- a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx +++ b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { AppMountParameters } from '../../../../../../src/core/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortalProps } from './types'; -interface HeaderMenuPortalProps { - children: ReactNode; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -export function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) { +// eslint-disable-next-line import/no-default-export +export default function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) { const portalNode = useMemo(() => createPortalNode(), []); useEffect(() => { diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx new file mode 100644 index 0000000000000..6e3835129beb2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { lazy, Suspense } from 'react'; +import { CoreVitalProps, HeaderMenuPortalProps } from './types'; + +export function getCoreVitalsComponent(props: CoreVitalProps) { + const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); + return ( + + + + ); +} + +export function HeaderMenuPortal(props: HeaderMenuPortalProps) { + const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/types.ts b/x-pack/plugins/observability/public/components/shared/types.ts new file mode 100644 index 0000000000000..9039f444f550f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactNode } from 'react'; +import { AppMountParameters } from '../../../../../../src/core/public'; +import { UXMetrics } from './core_web_vitals'; + +export interface HeaderMenuPortalProps { + children: ReactNode; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +} + +export interface CoreVitalProps { + loading: boolean; + data?: UXMetrics | null; + displayServiceName?: boolean; + serviceName?: string; + totalPageViews?: number; + displayTrafficMetric?: boolean; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 22cc5faf23967..c052541956c13 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -6,8 +6,7 @@ import { PluginInitializerContext, PluginInitializer } from 'kibana/public'; import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin'; -export { HeaderMenuPortal } from './components/shared/header_menu_portal'; -export { ObservabilityPluginSetup, ObservabilityPluginStart }; +export type { ObservabilityPluginSetup, ObservabilityPluginStart }; export const plugin: PluginInitializer = ( context: PluginInitializerContext @@ -17,7 +16,8 @@ export const plugin: PluginInitializer Date: Mon, 25 Jan 2021 16:20:14 +0000 Subject: [PATCH 42/60] [ML] Add ML deep links to navigational search (#88958) * [ML] Add ML deep links to navigational search * [ML] Refactor register helper files * [ML] Edit import in search_deep_links * [ML] Move register_feature out of register_helper * [ML] Add comment about registerFeature Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/public/plugin.ts | 37 +++--- .../ml/public/register_helper/index.ts | 10 ++ .../register_search_links/index.ts} | 5 +- .../register_search_links.ts | 27 +++++ .../search_deep_links.ts | 110 ++++++++++++++++++ 5 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/ml/public/register_helper/index.ts rename x-pack/plugins/ml/public/{register_helper.ts => register_helper/register_search_links/index.ts} (51%) create mode 100644 x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts create mode 100644 x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 1cc69ac2239ab..7c32671be93c4 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -70,7 +70,7 @@ export interface MlSetupDependencies { export type MlCoreSetup = CoreSetup; export class MlPlugin implements Plugin { - private appUpdater = new BehaviorSubject(() => ({})); + private appUpdater$ = new BehaviorSubject(() => ({})); private urlGenerator: undefined | UrlGeneratorContract; constructor(private initializerContext: PluginInitializerContext) {} @@ -85,7 +85,7 @@ export class MlPlugin implements Plugin { euiIconType: PLUGIN_ICON_SOLUTION, appRoute: '/app/ml', category: DEFAULT_APP_CATEGORIES.kibana, - updater$: this.appUpdater, + updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -133,23 +133,34 @@ export class MlPlugin implements Plugin { }); } else { // if ml is disabled in elasticsearch, disable ML in kibana - this.appUpdater.next(() => ({ + this.appUpdater$.next(() => ({ status: AppStatus.inaccessible, })); } // register various ML plugin features which require a full license - const { registerEmbeddables, registerManagementSection, registerMlUiActions } = await import( - './register_helper' - ); - - if (isMlEnabled(license) && isFullLicense(license)) { - const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; - if (canManageMLJobs && pluginsSetup.management !== undefined) { - registerManagementSection(pluginsSetup.management, core).enable(); + // note including registerFeature in register_helper would cause the page bundle size to increase significantly + const { + registerEmbeddables, + registerManagementSection, + registerMlUiActions, + registerSearchLinks, + } = await import('./register_helper'); + + const mlEnabled = isMlEnabled(license); + const fullLicense = isFullLicense(license); + if (mlEnabled) { + registerSearchLinks(this.appUpdater$, fullLicense); + + if (fullLicense) { + const canManageMLJobs = + capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; + if (canManageMLJobs && pluginsSetup.management !== undefined) { + registerManagementSection(pluginsSetup.management, core).enable(); + } + registerEmbeddables(pluginsSetup.embeddable, core); + registerMlUiActions(pluginsSetup.uiActions, core); } - registerEmbeddables(pluginsSetup.embeddable, core); - registerMlUiActions(pluginsSetup.uiActions, core); } }); diff --git a/x-pack/plugins/ml/public/register_helper/index.ts b/x-pack/plugins/ml/public/register_helper/index.ts new file mode 100644 index 0000000000000..8e62b6562520a --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerEmbeddables } from '../embeddables'; +export { registerManagementSection } from '../application/management'; +export { registerMlUiActions } from '../ui_actions'; +export { registerSearchLinks } from './register_search_links'; diff --git a/x-pack/plugins/ml/public/register_helper.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts similarity index 51% rename from x-pack/plugins/ml/public/register_helper.ts rename to x-pack/plugins/ml/public/register_helper/register_search_links/index.ts index 50ec53a10ece9..e1912c7ebabeb 100644 --- a/x-pack/plugins/ml/public/register_helper.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerEmbeddables } from './embeddables'; -export { registerFeature } from './register_feature'; -export { registerManagementSection } from './application/management'; -export { registerMlUiActions } from './ui_actions'; +export { registerSearchLinks } from './register_search_links'; diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts new file mode 100644 index 0000000000000..2df7e8140698a --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; + +import { AppUpdater } from 'src/core/public'; +import { getSearchDeepLinks } from './search_deep_links'; + +export function registerSearchLinks( + appUpdater: BehaviorSubject, + isFullLicense: boolean +) { + appUpdater.next(() => ({ + meta: { + keywords: [ + i18n.translate('xpack.ml.keyword.ml', { + defaultMessage: 'ML', + }), + ], + searchDeepLinks: getSearchDeepLinks(isFullLicense), + }, + })); +} diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts new file mode 100644 index 0000000000000..7108fb7af5670 --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import type { AppSearchDeepLink } from 'src/core/public'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; + +const OVERVIEW_LINK_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlOverviewSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.overview', { + defaultMessage: 'Overview', + }), + path: `/${ML_PAGES.OVERVIEW}`, +}; + +const ANOMALY_DETECTION_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlAnomalyDetectionSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.anomalyDetection', { + defaultMessage: 'Anomaly Detection', + }), + path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`, +}; + +const DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlDataFrameAnalyticsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.dataFrameAnalytics', { + defaultMessage: 'Data Frame Analytics', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`, + searchDeepLinks: [ + { + id: 'mlTrainedModelsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.trainedModels', { + defaultMessage: 'Trained Models', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE}`, + }, + ], +}; + +const DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlDataVisualizerSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.dataVisualizer', { + defaultMessage: 'Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER}`, +}; + +const FILE_UPLOAD_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlFileUploadSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.fileUpload', { + defaultMessage: 'File Upload', + }), + path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`, +}; + +const INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlIndexDataVisualizerSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.indexDataVisualizer', { + defaultMessage: 'Index Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`, +}; + +const SETTINGS_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.settings', { + defaultMessage: 'Settings', + }), + path: `/${ML_PAGES.SETTINGS}`, + searchDeepLinks: [ + { + id: 'mlCalendarSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.calendarSettings', { + defaultMessage: 'Calendars', + }), + path: `/${ML_PAGES.CALENDARS_MANAGE}`, + }, + { + id: 'mlFilterListsSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.filterListsSettings', { + defaultMessage: 'Filter Lists', + }), + path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists. + }, + ], +}; + +export function getSearchDeepLinks(isFullLicense: boolean) { + const deepLinks: AppSearchDeepLink[] = [ + DATA_VISUALIZER_SEARCH_DEEP_LINK, + FILE_UPLOAD_SEARCH_DEEP_LINK, + INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK, + ]; + + if (isFullLicense === true) { + deepLinks.push( + OVERVIEW_LINK_SEARCH_DEEP_LINK, + ANOMALY_DETECTION_SEARCH_DEEP_LINK, + DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK, + SETTINGS_SEARCH_DEEP_LINK + ); + } + + return deepLinks; +} From 43db7e365ff53e9014934910c34ee647eac29955 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 25 Jan 2021 11:27:59 -0500 Subject: [PATCH 43/60] [Search Profiler] Migrate server to new es-js client (#88725) --- .../searchprofiler/server/routes/profile.ts | 20 +++---- x-pack/test/api_integration/apis/index.ts | 1 + .../apis/searchprofiler/index.ts | 13 +++++ .../apis/searchprofiler/searchprofiler.ts | 56 +++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/api_integration/apis/searchprofiler/index.ts create mode 100644 x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts index 914c688a080f8..87f2ec1df1c92 100644 --- a/x-pack/plugins/searchprofiler/server/routes/profile.ts +++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts @@ -27,10 +27,6 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = }); } - const { - core: { elasticsearch }, - } = ctx; - const { body: { query, index }, } = request; @@ -46,21 +42,25 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = body: JSON.stringify(parsed, null, 2), }; try { - const resp = await elasticsearch.legacy.client.callAsCurrentUser('search', body); + const client = ctx.core.elasticsearch.client.asCurrentUser; + const resp = await client.search(body); + return response.ok({ body: { ok: true, - resp, + resp: resp.body, }, }); } catch (err) { log.error(err); + const { statusCode, body: errorBody } = err; + return response.customError({ - statusCode: err.status || 500, - body: err.body + statusCode: statusCode || 500, + body: errorBody ? { - message: err.message, - attributes: err.body, + message: errorBody.error?.reason, + attributes: errorBody, } : err, }); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 6b6326df017aa..2cd2654cffe3e 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -33,5 +33,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./searchprofiler')); }); } diff --git a/x-pack/test/api_integration/apis/searchprofiler/index.ts b/x-pack/test/api_integration/apis/searchprofiler/index.ts new file mode 100644 index 0000000000000..36794feb00d1b --- /dev/null +++ b/x-pack/test/api_integration/apis/searchprofiler/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Search Profiler', () => { + loadTestFile(require.resolve('./searchprofiler')); + }); +} diff --git a/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts new file mode 100644 index 0000000000000..041cfb82520b4 --- /dev/null +++ b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const API_BASE_PATH = '/api/searchprofiler'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Profile', () => { + it('should return profile results for a valid index', async () => { + const payload = { + index: '_all', + query: { + query: { + match_all: {}, + }, + }, + }; + + const { body } = await supertest + .post(`${API_BASE_PATH}/profile`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(payload) + .expect(200); + + expect(body.ok).to.eql(true); + }); + + it('should return error for invalid index', async () => { + const payloadWithInvalidIndex = { + index: 'index_does_not_exist', + query: { + query: { + match_all: {}, + }, + }, + }; + + const { body } = await supertest + .post(`${API_BASE_PATH}/execute`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(payloadWithInvalidIndex) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); +} From 592d79c210f7a614c37f1773ae4d4feef003f0e8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Jan 2021 17:07:13 +0000 Subject: [PATCH 44/60] skip failing es promotion suite (#89180) --- .../cross_cluster_replication/feature_controls/ccr_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts index 6b4b9c61151ba..4e234dd8e5501 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts @@ -13,7 +13,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('security', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/89180 + describe.skip('security', () => { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.common.navigateToApp('home'); From 342f9fbac15ce118ad088d2908c263d36ec23be4 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Jan 2021 17:11:52 +0000 Subject: [PATCH 45/60] skip failing es promotion suite (#89181) --- .../feature_controls/remote_clusters_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts index b1edc74607161..e6b5a7ac77d7c 100644 --- a/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts +++ b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts @@ -13,7 +13,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('security', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/89181 + describe.skip('security', () => { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.common.navigateToApp('home'); From edb5e35151a8fb5b168cb600cf3fdda94407177e Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 25 Jan 2021 13:26:53 -0500 Subject: [PATCH 46/60] [Painless Lab] Update server to use new es-js client (#88704) --- .../errors/handle_es_error.ts | 12 ++++- .../painless_lab/server/routes/api/execute.ts | 23 ++++----- .../painless_lab/server/shared_imports.ts | 2 +- x-pack/test/api_integration/apis/index.ts | 1 + .../apis/painless_lab/index.ts | 13 +++++ .../apis/painless_lab/painless_lab.ts | 49 +++++++++++++++++++ 6 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 x-pack/test/api_integration/apis/painless_lab/index.ts create mode 100644 x-pack/test/api_integration/apis/painless_lab/painless_lab.ts diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b7f7a5abb82b0..e2e1d7f05851c 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -13,14 +13,24 @@ import { IKibanaResponse, KibanaResponseFactory } from 'kibana/server'; interface EsErrorHandlerParams { error: ApiError; response: KibanaResponseFactory; + handleCustomError?: () => IKibanaResponse; } /* * For errors returned by the new elasticsearch js client. */ -export const handleEsError = ({ error, response }: EsErrorHandlerParams): IKibanaResponse => { +export const handleEsError = ({ + error, + response, + handleCustomError, +}: EsErrorHandlerParams): IKibanaResponse => { // error.name is slightly better in terms of performance, since all errors now have name property if (error.name === 'ResponseError') { + // The consumer may sometimes want to provide a custom response + if (typeof handleCustomError === 'function') { + return handleCustomError(); + } + const { statusCode, body } = error as ResponseError; return response.customError({ statusCode, diff --git a/x-pack/plugins/painless_lab/server/routes/api/execute.ts b/x-pack/plugins/painless_lab/server/routes/api/execute.ts index 7bb2d9f93c6fc..44529d7bacbd4 100644 --- a/x-pack/plugins/painless_lab/server/routes/api/execute.ts +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; -import { isEsError } from '../../shared_imports'; +import { handleEsError } from '../../shared_imports'; const bodySchema = schema.string(); @@ -23,23 +23,24 @@ export function registerExecuteRoute({ router, license }: RouteDependencies) { const body = req.body; try { - const callAsCurrentUser = ctx.core.elasticsearch.legacy.client.callAsCurrentUser; - const response = await callAsCurrentUser('scriptsPainlessExecute', { + const client = ctx.core.elasticsearch.client.asCurrentUser; + const response = await client.scriptsPainlessExecute({ body, }); return res.ok({ - body: response, + body: response.body, }); - } catch (e) { - if (isEsError(e)) { - // Assume invalid painless script was submitted - // Return 200 with error object + } catch (error) { + // Assume invalid painless script was submitted + // Return 200 with error object + const handleCustomError = () => { return res.ok({ - body: e.body, + body: error.body, }); - } - return res.internalError({ body: e }); + }; + + return handleEsError({ error, response: res, handleCustomError }); } }) ); diff --git a/x-pack/plugins/painless_lab/server/shared_imports.ts b/x-pack/plugins/painless_lab/server/shared_imports.ts index 454beda5394c7..068cddcee4c86 100644 --- a/x-pack/plugins/painless_lab/server/shared_imports.ts +++ b/x-pack/plugins/painless_lab/server/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 2cd2654cffe3e..ea42b583fc00e 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -34,5 +34,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./searchprofiler')); + loadTestFile(require.resolve('./painless_lab')); }); } diff --git a/x-pack/test/api_integration/apis/painless_lab/index.ts b/x-pack/test/api_integration/apis/painless_lab/index.ts new file mode 100644 index 0000000000000..7ccd7c5691e56 --- /dev/null +++ b/x-pack/test/api_integration/apis/painless_lab/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Painless Lab', () => { + loadTestFile(require.resolve('./painless_lab')); + }); +} diff --git a/x-pack/test/api_integration/apis/painless_lab/painless_lab.ts b/x-pack/test/api_integration/apis/painless_lab/painless_lab.ts new file mode 100644 index 0000000000000..c78ceaaa81437 --- /dev/null +++ b/x-pack/test/api_integration/apis/painless_lab/painless_lab.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const API_BASE_PATH = '/api/painless_lab'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Painless Lab', function () { + describe('Execute', () => { + it('should execute a valid painless script', async () => { + const script = + '"{\\n \\"script\\": {\\n \\"source\\": \\"return true;\\",\\n \\"params\\": {\\n \\"string_parameter\\": \\"string value\\",\\n \\"number_parameter\\": 1.5,\\n \\"boolean_parameter\\": true\\n}\\n }\\n}"'; + + const { body } = await supertest + .post(`${API_BASE_PATH}/execute`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(script) + .expect(200); + + expect(body).to.eql({ + result: 'true', + }); + }); + + it('should return error response for invalid painless script', async () => { + const invalidScript = + '"{\\n \\"script\\": {\\n \\"source\\": \\"foobar\\",\\n \\"params\\": {\\n \\"string_parameter\\": \\"string value\\",\\n \\"number_parameter\\": 1.5,\\n \\"boolean_parameter\\": true\\n}\\n }\\n}"'; + + const { body } = await supertest + .post(`${API_BASE_PATH}/execute`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(invalidScript) + .expect(200); + + expect(body.error).to.not.be(undefined); + expect(body.error.reason).to.eql('compile error'); + }); + }); + }); +} From 7d50fbbf6d8ab748e439503a554014144c6a7f4c Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 25 Jan 2021 13:57:56 -0500 Subject: [PATCH 47/60] [CI] [TeamCity] Build master and 7.x hourly, others daily/on merge (#89184) --- .teamcity/src/Common.kt | 8 +++++++ .teamcity/src/builds/DailyCi.kt | 37 +++++++++++++++++++++++++++++++ .teamcity/src/builds/OnMergeCi.kt | 34 ++++++++++++++++++++++++++++ .teamcity/src/projects/Kibana.kt | 12 +++++++++- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .teamcity/src/builds/DailyCi.kt create mode 100644 .teamcity/src/builds/OnMergeCi.kt diff --git a/.teamcity/src/Common.kt b/.teamcity/src/Common.kt index 2e5357541bfe7..de3f96a5c790f 100644 --- a/.teamcity/src/Common.kt +++ b/.teamcity/src/Common.kt @@ -22,6 +22,14 @@ fun isReportingEnabled(): Boolean { return ENABLE_REPORTING; } +// master and 7.x get committed to so often, we only want to run full CI for them hourly +// but for other branches, we can run daily and on merge +fun isHourlyOnlyBranch(): Boolean { + val branch = getProjectBranch() + + return branch == "master" || branch.matches("""^[0-9]+\.x$""".toRegex()) +} + fun makeSafeId(id: String): String { return id.replace(Regex("[^a-zA-Z0-9_]"), "_") } diff --git a/.teamcity/src/builds/DailyCi.kt b/.teamcity/src/builds/DailyCi.kt new file mode 100644 index 0000000000000..9a8f25f5ba014 --- /dev/null +++ b/.teamcity/src/builds/DailyCi.kt @@ -0,0 +1,37 @@ +package builds + +import addSlackNotifications +import areTriggersEnabled +import dependsOn +import getProjectBranch +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.schedule + +object DailyCi : BuildType({ + id("Daily_CI") + name = "Daily CI" + description = "Runs everything in CI, daily" + type = Type.COMPOSITE + paused = !areTriggersEnabled() + + triggers { + schedule { + schedulingPolicy = cron { + hours = "0" + minutes = "0" + } + branchFilter = "refs/heads/${getProjectBranch()}" + triggerBuild = always() + withPendingChangesOnly = false + } + } + + dependsOn( + FullCi + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/OnMergeCi.kt b/.teamcity/src/builds/OnMergeCi.kt new file mode 100644 index 0000000000000..174b73d53de61 --- /dev/null +++ b/.teamcity/src/builds/OnMergeCi.kt @@ -0,0 +1,34 @@ +package builds + +import addSlackNotifications +import areTriggersEnabled +import dependsOn +import getProjectBranch +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs + +object OnMergeCi : BuildType({ + id("OnMerge_CI") + name = "On Merge CI" + description = "Runs everything in CI, on each commit" + type = Type.COMPOSITE + paused = !areTriggersEnabled() + + maxRunningBuilds = 1 + + triggers { + vcs { + perCheckinTriggering = false + branchFilter = "refs/heads/${getProjectBranch()}" + } + } + + dependsOn( + FullCi + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt index fe04d4f5ab36e..5cddcf18e067f 100644 --- a/.teamcity/src/projects/Kibana.kt +++ b/.teamcity/src/projects/Kibana.kt @@ -7,6 +7,7 @@ import builds.oss.* import builds.test.* import CloudProfile import co.elastic.teamcity.common.googleCloudProfile +import isHourlyOnlyBranch import jetbrains.buildServer.configs.kotlin.v2019_2.* import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.slackConnection import templates.KibanaTemplate @@ -136,7 +137,16 @@ fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { buildType(FullCi) buildType(BaselineCi) - buildType(HourlyCi) + + // master and 7.x get committed to so often, we only want to run full CI for them hourly + // but for other branches, we can run daily and on merge + if (isHourlyOnlyBranch()) { + buildType(HourlyCi) + } else { + buildType(DailyCi) + buildType(OnMergeCi) + } + buildType(PullRequestCi) } From 15a45e8c38ac4b371f700285ca35591f918e3ddf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Jan 2021 12:25:52 -0700 Subject: [PATCH 48/60] [Maps] fix users without access to Maps should not have the option to create them (#88830) * [Maps] fix users without access to Maps should not have the option to create them * fix test message * wrap add geo field trigger in show capabilities check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/public/plugin.ts | 10 ++- .../apps/maps/visualize_create_menu.js | 82 ++++++++++++------- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 8bffea3ce5a87..690002a771601 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -160,11 +160,19 @@ export class MapsPlugin public start(core: CoreStart, plugins: MapsPluginStartDependencies): MapsStartApi { setLicensingPluginStart(plugins.licensing); - plugins.uiActions.addTriggerAction(VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldAction); setStartServices(core, plugins); + // unregisters the OSS alias plugins.visualizations.unRegisterAlias(PLUGIN_ID_OSS); + if (core.application.capabilities.maps.show) { + plugins.uiActions.addTriggerAction(VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldAction); + } + + if (!core.application.capabilities.maps.save) { + plugins.visualizations.unRegisterAlias(APP_ID); + } + return { createLayerDescriptors, registerLayerWizard, diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js index 549901884d39b..f58856752dcd2 100644 --- a/x-pack/test/functional/apps/maps/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -12,42 +12,66 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); describe('visualize create menu', () => { - before(async () => { - await security.testUser.setRoles( - ['test_logstash_reader', 'global_maps_all', 'geoshape_data_reader', 'global_visualize_all'], - false - ); + describe('maps visualize alias', () => { + describe('with write permission', () => { + before(async () => { + await security.testUser.setRoles(['global_maps_all', 'global_visualize_all'], false); - await PageObjects.visualize.navigateToNewVisualization(); - }); + await PageObjects.visualize.navigateToNewVisualization(); + }); - after(async () => { - await security.testUser.restoreDefaults(); - }); + it('should show maps application in create menu', async () => { + const hasMapsApp = await PageObjects.visualize.hasMapsApp(); + expect(hasMapsApp).to.equal(true); + }); - it('should show maps application in create menu', async () => { - const hasMapsApp = await PageObjects.visualize.hasMapsApp(); - expect(hasMapsApp).to.equal(true); - }); + it('should take users to Maps application when Maps is clicked', async () => { + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('Road map'); + expect(doesLayerExist).to.equal(true); + }); + }); - it('should not show legacy region map visualizion in create menu', async () => { - await PageObjects.visualize.clickAggBasedVisualizations(); - const hasLegecyViz = await PageObjects.visualize.hasRegionMap(); - expect(hasLegecyViz).to.equal(false); - }); + describe('without write permission', () => { + before(async () => { + await security.testUser.setRoles(['global_maps_read', 'global_visualize_all'], false); + + await PageObjects.visualize.navigateToNewVisualization(); + }); - it('should not show legacy tilemap map visualizion in create menu', async () => { - const hasLegecyViz = await PageObjects.visualize.hasTileMap(); - expect(hasLegecyViz).to.equal(false); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not show maps application in create menu', async () => { + const hasMapsApp = await PageObjects.visualize.hasMapsApp(); + expect(hasMapsApp).to.equal(false); + }); + }); }); - it('should take users to Maps application when Maps is clicked', async () => { - await PageObjects.visualize.goBackToGroups(); - await PageObjects.visualize.clickMapsApp(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.maps.waitForLayersToLoad(); - const doesLayerExist = await PageObjects.maps.doesLayerExist('Road map'); - expect(doesLayerExist).to.equal(true); + describe('aggregion based visualizations', () => { + before(async () => { + await security.testUser.setRoles(['global_visualize_all'], false); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not show legacy region map visualizion in create menu', async () => { + const hasLegecyViz = await PageObjects.visualize.hasRegionMap(); + expect(hasLegecyViz).to.equal(false); + }); + + it('should not show legacy tilemap map visualizion in create menu', async () => { + const hasLegecyViz = await PageObjects.visualize.hasTileMap(); + expect(hasLegecyViz).to.equal(false); + }); }); }); } From 62b32c0a2a0dc544a8c35462e00d5a278c11574b Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 25 Jan 2021 14:28:26 -0500 Subject: [PATCH 49/60] Remove onAppLeave in useEffectCleanup to prevent confirm when leaving from listing page (#89041) --- .../public/application/components/visualize_top_nav.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index a1b6aac30d813..b0d931c6c87fa 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -147,6 +147,10 @@ const TopNav = ({ } return actions.default(); }); + return () => { + // reset on app leave handler so leaving from the listing page doesn't trigger a confirmation + onAppLeave((actions) => actions.default()); + }; }, [ onAppLeave, originatingApp, From e5588a129b1a0b2796822d4773176cc712dd5318 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 25 Jan 2021 13:48:35 -0600 Subject: [PATCH 50/60] Move EUI styled components integration to kibana_react (#86065) ...from xpack_legacy. Remove the duplicated typings from the observability plugin and only use the ones from kibana_react. Fixes #78248. --- .../common/eui_styled_components.tsx | 6 ++- src/plugins/kibana_react/common/index.ts | 9 ++++ src/plugins/kibana_react/kibana.json | 3 +- src/plugins/kibana_react/tsconfig.json | 9 +--- .../apm/common/service_health_status.ts | 3 +- .../ExceptionStacktrace.stories.tsx | 2 +- .../__stories__/MapTooltip.stories.tsx | 2 +- .../app/RumDashboard/utils/test_helper.tsx | 2 +- .../ServiceMap/Popover/Popover.stories.tsx | 2 +- .../Popover/service_stats_list.stories.tsx | 2 +- .../__stories__/Cytoscape.stories.tsx | 2 +- .../cytoscape_example_data.stories.tsx | 2 +- .../app/ServiceMap/cytoscape_options.ts | 2 +- .../components/app/ServiceMap/index.test.tsx | 4 +- .../use_cytoscape_event_handlers.test.tsx | 3 +- .../use_cytoscape_event_handlers.ts | 3 +- .../index.stories.tsx | 2 +- .../TransactionDetails/Distribution/index.tsx | 2 +- .../WaterfallContainer.stories.tsx | 2 +- .../service_inventory.test.tsx | 4 +- .../Timeline/Marker/AgentMarker.test.tsx | 2 +- .../shared/time_comparison/index.test.tsx | 2 +- x-pack/plugins/apm/public/hooks/use_theme.tsx | 2 +- .../selectors/latency_chart_selector.test.ts | 2 +- .../selectors/latency_chart_selectors.ts | 2 +- .../throuput_chart_selectors.test.ts | 2 +- .../selectors/throuput_chart_selectors.ts | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 2 +- .../fleet/public/applications/fleet/app.tsx | 2 +- .../inventory_models/aws_ec2/layout.tsx | 3 +- .../inventory_models/aws_rds/layout.tsx | 3 +- .../common/inventory_models/aws_s3/layout.tsx | 3 +- .../inventory_models/aws_sqs/layout.tsx | 3 +- .../inventory_models/container/layout.tsx | 3 +- .../common/inventory_models/host/layout.tsx | 3 +- .../common/inventory_models/pod/layout.tsx | 3 +- .../inventory_models/shared/layouts/aws.tsx | 3 +- .../inventory_models/shared/layouts/nginx.tsx | 3 +- .../inventory/components/expression.tsx | 2 +- .../components/expression_row.tsx | 2 +- .../infra/public/apps/common_providers.tsx | 2 +- .../autocomplete_field/autocomplete_field.tsx | 2 +- .../autocomplete_field/suggestion_item.tsx | 2 +- .../components/centered_flyout_body.tsx | 2 +- .../data_search_error_callout.stories.tsx | 2 +- .../data_search_progress.stories.tsx | 2 +- .../components/empty_states/no_data.tsx | 2 +- .../components/empty_states/no_indices.tsx | 2 +- .../infra/public/components/error_page.tsx | 2 +- .../public/components/eui/toolbar/toolbar.tsx | 2 +- .../public/components/fixed_datepicker.tsx | 2 +- .../infra/public/components/loading/index.tsx | 2 +- .../components/loading_overlay_wrapper.tsx | 2 +- .../public/components/log_stream/index.tsx | 2 +- .../log_stream/log_stream.stories.mdx | 2 +- .../quality_warning_notices.stories.tsx | 2 +- .../quality_warning_notices.tsx | 2 +- .../initial_configuration_step.stories.tsx | 2 +- .../missing_results_privileges_prompt.tsx | 2 +- .../missing_setup_privileges_prompt.tsx | 2 +- .../ml_unavailable_prompt.tsx | 2 +- .../logging/log_analysis_setup/setup_page.tsx | 2 +- .../setup_status_unknown_prompt.tsx | 2 +- .../subscription_splash_content.tsx | 2 +- .../logging/log_customization_menu.tsx | 2 +- .../log_entry_examples/log_entry_examples.tsx | 2 +- .../logging/log_highlights_menu.tsx | 2 +- .../logging/log_minimap/density_chart.tsx | 2 +- .../log_minimap/highlighted_interval.tsx | 2 +- .../logging/log_minimap/log_minimap.tsx | 2 +- .../logging/log_minimap/search_marker.tsx | 2 +- .../logging/log_minimap/time_ruler.tsx | 2 +- .../log_search_controls/log_search_input.tsx | 2 +- .../components/logging/log_statusbar.tsx | 2 +- .../log_text_stream/column_headers.tsx | 2 +- .../logging/log_text_stream/field_value.tsx | 2 +- .../logging/log_text_stream/highlighting.tsx | 2 +- .../logging/log_text_stream/jump_to_tail.tsx | 2 +- .../log_text_stream/loading_item_view.tsx | 2 +- .../log_text_stream/log_entry_column.tsx | 2 +- .../log_entry_context_menu.tsx | 2 +- .../log_entry_field_column.test.tsx | 2 +- .../log_entry_field_column.tsx | 2 +- .../log_entry_message_column.test.tsx | 2 +- .../log_entry_message_column.tsx | 2 +- .../logging/log_text_stream/log_entry_row.tsx | 3 +- .../log_entry_timestamp_column.tsx | 2 +- .../scrollable_log_text_stream_view.tsx | 2 +- .../logging/log_text_stream/text_styles.tsx | 2 +- .../log_text_stream/vertical_scroll_panel.tsx | 2 +- .../components/navigation/app_navigation.tsx | 2 +- .../components/navigation/routed_tabs.tsx | 2 +- .../plugins/infra/public/components/page.tsx | 2 +- .../infra/public/components/toolbar_panel.ts | 2 +- x-pack/plugins/infra/public/pages/error.tsx | 2 +- .../page_results_content.tsx | 3 +- .../top_categories/category_expression.tsx | 2 +- .../sections/top_categories/datasets_list.tsx | 2 +- .../single_metric_comparison.tsx | 2 +- .../top_categories/top_categories_table.tsx | 2 +- .../log_entry_rate/page_results_content.tsx | 3 +- .../sections/anomalies/expanded_row.tsx | 2 +- .../sections/anomalies/index.tsx | 2 +- .../sections/anomalies/log_entry_example.tsx | 2 +- .../logs/settings/add_log_column_popover.tsx | 2 +- .../pages/logs/stream/page_logs_content.tsx | 2 +- .../logs/stream/page_view_log_in_context.tsx | 2 +- .../components/bottom_drawer.tsx | 4 +- .../components/dropdown_button.tsx | 2 +- .../inventory_view/components/layout.tsx | 2 +- .../subscription_splash_content.tsx | 2 +- .../components/node_details/overlay.tsx | 2 +- .../tabs/processes/process_row.tsx | 2 +- .../tabs/processes/process_row_charts.tsx | 2 +- .../tabs/processes/processes_table.tsx | 2 +- .../tabs/processes/summary_table.tsx | 2 +- .../node_details/tabs/properties/index.tsx | 2 +- .../components/node_details/tabs/shared.tsx | 2 +- .../components/nodes_overview.tsx | 2 +- .../components/timeline/timeline.tsx | 2 +- .../waffle/conditional_tooltip.test.tsx | 2 +- .../components/waffle/conditional_tooltip.tsx | 2 +- .../components/waffle/gradient_legend.tsx | 2 +- .../components/waffle/group_name.tsx | 2 +- .../components/waffle/group_of_groups.tsx | 2 +- .../components/waffle/group_of_nodes.tsx | 2 +- .../components/waffle/legend.tsx | 2 +- .../components/waffle/legend_controls.tsx | 2 +- .../inventory_view/components/waffle/map.tsx | 2 +- .../metric_control/custom_metric_form.tsx | 2 +- .../metric_control/metrics_edit_mode.tsx | 2 +- .../waffle/metric_control/mode_switcher.tsx | 2 +- .../inventory_view/components/waffle/node.tsx | 2 +- .../components/waffle/node_context_menu.tsx | 3 +- .../components/waffle/palette_preview.tsx | 2 +- .../waffle/stepped_gradient_legend.tsx | 2 +- .../components/waffle/steps_legend.tsx | 2 +- .../waffle/waffle_group_by_controls.tsx | 2 +- .../waffle/waffle_sort_controls.tsx | 2 +- .../waffle/waffle_time_controls.tsx | 2 +- .../components/gauges_section_vis.tsx | 2 +- .../metric_detail/components/invalid_node.tsx | 2 +- .../components/layout_content.tsx | 2 +- .../components/metadata_details.tsx | 2 +- .../components/node_details_page.tsx | 2 +- .../metric_detail/components/side_nav.tsx | 2 +- .../components/time_controls.tsx | 2 +- .../pages/metrics/metric_detail/index.tsx | 6 ++- .../pages/metrics/metric_detail/types.ts | 2 +- .../metrics_explorer/components/chart.tsx | 2 +- .../public/application/index.tsx | 2 +- .../__stories__/core_vitals.stories.tsx | 2 +- .../observability/public/hooks/use_theme.tsx | 2 +- .../pages/overview/overview.stories.tsx | 2 +- .../public/typings/eui_styled_components.tsx | 47 ------------------- .../observability/public/typings/index.ts | 1 - .../public/utils/test_helper.tsx | 2 +- .../mock/endpoint/app_root_provider.tsx | 2 +- .../pages/policy/view/vertical_divider.ts | 2 +- .../plugins/uptime/public/apps/uptime_app.tsx | 2 +- .../ping_list/columns/ping_timestamp.tsx | 3 +- .../synthetics/waterfall/components/styles.ts | 2 +- .../waterfall/components/waterfall.test.tsx | 2 +- .../kuery_bar/typeahead/suggestion.tsx | 2 +- .../kuery_bar/typeahead/suggestions.tsx | 2 +- .../columns/monitor_status_column.test.tsx | 2 +- .../columns/monitor_status_column.tsx | 2 +- .../monitor_list/monitor_list.test.tsx | 2 +- .../uptime/public/lib/helper/rtl_helpers.tsx | 2 +- x-pack/plugins/xpack_legacy/common/index.ts | 7 --- 170 files changed, 193 insertions(+), 242 deletions(-) rename {x-pack/plugins/xpack_legacy => src/plugins/kibana_react}/common/eui_styled_components.tsx (86%) create mode 100644 src/plugins/kibana_react/common/index.ts delete mode 100644 x-pack/plugins/observability/public/typings/eui_styled_components.tsx delete mode 100644 x-pack/plugins/xpack_legacy/common/index.ts diff --git a/x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx b/src/plugins/kibana_react/common/eui_styled_components.tsx similarity index 86% rename from x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx rename to src/plugins/kibana_react/common/eui_styled_components.tsx index aab16f9d79c4b..fe002500309ad 100644 --- a/x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx +++ b/src/plugins/kibana_react/common/eui_styled_components.tsx @@ -1,7 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ import React from 'react'; diff --git a/src/plugins/kibana_react/common/index.ts b/src/plugins/kibana_react/common/index.ts new file mode 100644 index 0000000000000..0af8ed573699b --- /dev/null +++ b/src/plugins/kibana_react/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './eui_styled_components'; diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json index f2f0da53e6280..6bf7ff1d82070 100644 --- a/src/plugins/kibana_react/kibana.json +++ b/src/plugins/kibana_react/kibana.json @@ -2,5 +2,6 @@ "id": "kibanaReact", "version": "kibana", "ui": true, - "server": false + "server": false, + "extraPublicDirs": ["common"] } diff --git a/src/plugins/kibana_react/tsconfig.json b/src/plugins/kibana_react/tsconfig.json index 52899f868dbfb..eb9a24ca141f6 100644 --- a/src/plugins/kibana_react/tsconfig.json +++ b/src/plugins/kibana_react/tsconfig.json @@ -7,11 +7,6 @@ "declaration": true, "declarationMap": true }, - "include": [ - "public/**/*", - "../../../typings/**/*" - ], - "references": [ - { "path": "../kibana_utils/tsconfig.json" } - ] + "include": ["common/**/*", "public/**/*", "../../../typings/**/*"], + "references": [{ "path": "../kibana_utils/tsconfig.json" }] } diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts index f66e03a9733a3..c2ba42cc20760 100644 --- a/x-pack/plugins/apm/common/service_health_status.ts +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -5,10 +5,9 @@ */ import { i18n } from '@kbn/i18n'; +import { EuiTheme } from '../../../../src/plugins/kibana_react/common'; import { ANOMALY_SEVERITY } from '../../ml/common'; -import { EuiTheme } from '../../xpack_legacy/common'; - export enum ServiceHealthStatus { healthy = 'healthy', critical = 'critical', diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx index ff95d6fd1254c..a7b0b0393ea94 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx @@ -5,7 +5,7 @@ */ import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; import { ExceptionStacktrace } from './ExceptionStacktrace'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index 1053dd611d519..50615c61dea1f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { MapToolTip } from '../MapToolTip'; import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx index d5b8cd83d437c..ef44746ffd3f5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx @@ -11,7 +11,7 @@ import { of } from 'rxjs'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { UrlParamsProvider } from '../../../../context/url_params_context/url_params_context'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 313b262508c61..b36cb363f4c25 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -7,7 +7,7 @@ import cytoscape from 'cytoscape'; import { HttpSetup } from 'kibana/public'; import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/service_stats_list.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/service_stats_list.stories.tsx index 052f9e9515751..4212cdd29e81a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/service_stats_list.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/service_stats_list.stories.tsx @@ -5,7 +5,7 @@ */ import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { ServiceStatsList } from './ServiceStatsList'; export default { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx index a4985d2f5ab0c..e66eeb4759d50 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx @@ -7,7 +7,7 @@ import { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { Cytoscape } from '../Cytoscape'; import { iconForNode } from '../icons'; import { Centerer } from './centerer'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/cytoscape_example_data.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/cytoscape_example_data.stories.tsx index 0673735ba0adb..349d60f481bac 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/cytoscape_example_data.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/cytoscape_example_data.stories.tsx @@ -16,7 +16,7 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { ComponentType, useEffect, useState } from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; import exampleResponseHipsterStore from './example_response_hipster_store.json'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts index 9a0ebb7173c26..a1e49876d8291 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts @@ -5,7 +5,7 @@ */ import cytoscape from 'cytoscape'; import { CSSProperties } from 'react'; -import { EuiTheme } from '../../../../../observability/public'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index c6f82e3492750..7980543e41b16 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -8,9 +8,9 @@ import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; -import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../context/license/license_context'; import * as useFetcherModule from '../../../hooks/use_fetcher'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx index ab16da1410662..767186781aff2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx @@ -7,7 +7,8 @@ import { renderHook } from '@testing-library/react-hooks'; import cytoscape from 'cytoscape'; import dagre from 'cytoscape-dagre'; -import { EuiTheme, useUiTracker } from '../../../../../observability/public'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import { useUiTracker } from '../../../../../observability/public'; import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; import lodash from 'lodash'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts index a2b229780ead0..cf7ee50f242b9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts @@ -7,7 +7,8 @@ import cytoscape from 'cytoscape'; import { debounce } from 'lodash'; import { useEffect } from 'react'; -import { EuiTheme, useUiTracker } from '../../../../../observability/public'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import { useUiTracker } from '../../../../../observability/public'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; /* diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index 5ca643428e49c..a03b0b255e9f1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -13,6 +13,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { HttpSetup } from 'kibana/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; @@ -21,7 +22,6 @@ import { ApmPluginContext, ApmPluginContextValue, } from '../../../../../context/apm_plugin/apm_plugin_context'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; storiesOf( 'app/Settings/AgentConfigurations/AgentConfigurationCreateEdit', diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index a94c48f02c101..b7806774536fa 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -23,10 +23,10 @@ import d3 from 'd3'; import { isEmpty } from 'lodash'; import React from 'react'; import { ValuesType } from 'utility-types'; -import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import type { IUrlParams } from '../../../../context/url_params_context/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index 5217c2abb11dc..6e78d30bfa1d9 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -6,7 +6,7 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index eb8068bc8114d..7c2729dbb0a16 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -9,10 +9,10 @@ import { CoreStart } from 'kibana/public'; import { merge } from 'lodash'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; import { ServiceHealthStatus } from '../../../../common/service_health_status'; import { ServiceInventory } from '.'; -import { EuiThemeProvider } from '../../../../../observability/public'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx index 072b5b5bc7a96..aaaaf5174e4fd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { AgentMarker } from './AgentMarker'; import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; describe('AgentMarker', () => { const mark = { diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx index 6348097a3e3ad..6790301c13abf 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -6,7 +6,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { IUrlParams } from '../../../context/url_params_context/types'; import { diff --git a/x-pack/plugins/apm/public/hooks/use_theme.tsx b/x-pack/plugins/apm/public/hooks/use_theme.tsx index e372a764a9505..3c50424b219dd 100644 --- a/x-pack/plugins/apm/public/hooks/use_theme.tsx +++ b/x-pack/plugins/apm/public/hooks/use_theme.tsx @@ -6,7 +6,7 @@ import { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { EuiTheme } from '../../../observability/public'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; export function useTheme(): EuiTheme { const theme = useContext(ThemeContext); diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts index 7b0826fa76883..7214d0b99bd6a 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTheme } from '../../../xpack_legacy/common'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; import { LatencyAggregationType } from '../../common/latency_aggregation_types'; import { getLatencyChartSelector, diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts index dba698ffb1bc1..8ef57ac74be66 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts @@ -6,7 +6,7 @@ import { Fit } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { rgba } from 'polished'; -import { EuiTheme } from '../../../observability/public'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../common/utils/formatters'; import { APMChartSpec, Coordinate } from '../../typings/timeseries'; import { APIReturnType } from '../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts index 03877b9e5bff2..e5fed2ee2ae22 100644 --- a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTheme } from '../../../observability/public'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; import { getThrouputChartSelector, ThrouputChartsResponse, diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts index a392f247aec42..b21fcb99fbe1a 100644 --- a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts @@ -5,7 +5,7 @@ */ import { difference, zipObject } from 'lodash'; -import { EuiTheme } from '../../../observability/public'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; import { asTransactionRate } from '../../common/utils/formatters'; import { TimeSeries } from '../../typings/timeseries'; import { APIReturnType } from '../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 21c87c18be363..98745b4f6c636 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -14,12 +14,12 @@ import moment from 'moment'; import { Moment } from 'moment-timezone'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { ESFilter, ESSearchRequest, ESSearchResponse, } from '../../../../typings/elasticsearch'; -import { EuiThemeProvider } from '../../../observability/public'; import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index ed91c1cb1479c..21372508ee99d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -36,7 +36,7 @@ import { ProtectedRoute } from './index'; import { FleetConfigType, FleetStartServices } from '../../plugin'; import { UIExtensionsStorage } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../xpack_legacy/common'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { UIExtensionsContext } from './hooks/use_ui_extension'; const ErrorLayout = ({ children }: { children: JSX.Element }) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx index 9494e4aa396a5..348fb8b691b86 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -15,8 +15,7 @@ import { SubSection } from '../../../public/pages/metrics/metric_detail/componen import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx index 08b865f01b06c..45395eff1f823 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -13,8 +13,7 @@ import { Section } from '../../../public/pages/metrics/metric_detail/components/ import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx index e16f8ef6addde..cd78ed56d4bc9 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -13,8 +13,7 @@ import { Section } from '../../../public/pages/metrics/metric_detail/components/ import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx index ff13f2db104de..7f3b4a6d73bf8 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -13,8 +13,7 @@ import { Section } from '../../../public/pages/metrics/metric_detail/components/ import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; diff --git a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx index b9366a43e40c6..fc4293e698813 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx @@ -15,8 +15,7 @@ import { SubSection } from '../../../public/pages/metrics/metric_detail/componen import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx index e23118c747a9b..c238c813c8691 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx @@ -5,8 +5,7 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx index 271e32556ae28..255b9483f70c5 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx @@ -15,8 +15,7 @@ import { SubSection } from '../../../public/pages/metrics/metric_detail/componen import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../observability/public'; +import { withTheme } from '../../../../../../src/plugins/kibana_react/common'; import * as Nginx from '../shared/layouts/nginx'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx index 6f2791534c17e..f8e20ee6404f6 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx @@ -15,8 +15,7 @@ import { SubSection } from '../../../../public/pages/metrics/metric_detail/compo import { GaugesSectionVis } from '../../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../../observability/public'; +import { withTheme } from '../../../../../../../src/plugins/kibana_react/common'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx index cf3a06994cc96..3ec17d96043a5 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx @@ -13,8 +13,7 @@ import { Section } from '../../../../public/pages/metrics/metric_detail/componen import { SubSection } from '../../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { withTheme } from '../../../../../observability/public'; +import { withTheme } from '../../../../../../../src/plugins/kibana_react/common'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 3dc754822879d..f8102d9ead2cb 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -30,7 +30,7 @@ import { Comparator, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ThresholdExpression, ForLastExpression, diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx index 1487557bde3a0..cdab53c92d32c 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -14,7 +14,7 @@ import { ThresholdExpression, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; import { MetricExpression, AGGREGATION_TYPES } from '../types'; diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 41aa2fd898585..ebfa412410dd7 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -11,7 +11,7 @@ import { useUiSetting$, KibanaContextProvider, } from '../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../observability/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; import { createKibanaContextForPlugin } from '../hooks/use_kibana'; import { InfraClientStartDeps } from '../types'; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 08594c5ddba18..757a1a1e80ef3 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -7,7 +7,7 @@ import { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; import React from 'react'; import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { composeStateUpdaters } from '../../utils/typed_react'; import { SuggestionItem } from './suggestion_item'; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx index 4bcb7a7ec8a02..78b36d4e047e6 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; interface Props { diff --git a/x-pack/plugins/infra/public/components/centered_flyout_body.tsx b/x-pack/plugins/infra/public/components/centered_flyout_body.tsx index ec762610f36c4..83d1d4de2b2fa 100644 --- a/x-pack/plugins/infra/public/components/centered_flyout_body.tsx +++ b/x-pack/plugins/infra/public/components/centered_flyout_body.tsx @@ -5,7 +5,7 @@ */ import { EuiFlyoutBody } from '@elastic/eui'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; export const CenteredEuiFlyoutBody = euiStyled(EuiFlyoutBody)` & .euiFlyoutBody__overflow { diff --git a/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx b/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx index 4e46e5fdd3f45..32f9d86c1b904 100644 --- a/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx +++ b/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx @@ -7,7 +7,7 @@ import { PropsOf } from '@elastic/eui'; import { Meta, Story } from '@storybook/react/types-6-0'; import React from 'react'; -import { EuiThemeProvider } from '../../../observability/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { DataSearchErrorCallout } from './data_search_error_callout'; export default { diff --git a/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx index d5293a7282305..492aa865dfb61 100644 --- a/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx +++ b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx @@ -7,7 +7,7 @@ import { PropsOf } from '@elastic/eui'; import { Meta, Story } from '@storybook/react/types-6-0'; import React from 'react'; -import { EuiThemeProvider } from '../../../observability/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { DataSearchProgress } from './data_search_progress'; export default { diff --git a/x-pack/plugins/infra/public/components/empty_states/no_data.tsx b/x-pack/plugins/infra/public/components/empty_states/no_data.tsx index 97dc7ac1f8520..e4a9d82fe8c36 100644 --- a/x-pack/plugins/infra/public/components/empty_states/no_data.tsx +++ b/x-pack/plugins/infra/public/components/empty_states/no_data.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; interface NoDataProps { titleText: string; diff --git a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx index b453a4f0bc342..1e73c3abe338d 100644 --- a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx +++ b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; interface NoIndicesProps { message: string; diff --git a/x-pack/plugins/infra/public/components/error_page.tsx b/x-pack/plugins/infra/public/components/error_page.tsx index 5c8b576c161d7..c800c75a77120 100644 --- a/x-pack/plugins/infra/public/components/error_page.tsx +++ b/x-pack/plugins/infra/public/components/error_page.tsx @@ -15,7 +15,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { FlexPage } from './page'; interface Props { diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx index 912550b90b9b9..9ac1996f94b97 100644 --- a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -6,7 +6,7 @@ import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; export const Toolbar = euiStyled(EuiPanel).attrs(() => ({ grow: false, diff --git a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx index fcebb89561826..f4b34b65711cb 100644 --- a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx +++ b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; export const FixedDatePicker = euiStyled( ({ diff --git a/x-pack/plugins/infra/public/components/loading/index.tsx b/x-pack/plugins/infra/public/components/loading/index.tsx index 8c25dfc2380f9..e6a7aa88391f3 100644 --- a/x-pack/plugins/infra/public/components/loading/index.tsx +++ b/x-pack/plugins/infra/public/components/loading/index.tsx @@ -7,7 +7,7 @@ import { EuiLoadingChart, EuiPanel, EuiText } from '@elastic/eui'; import * as React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; interface InfraLoadingProps { text: string | JSX.Element; diff --git a/x-pack/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/plugins/infra/public/components/loading_overlay_wrapper.tsx index 3b22ee24cee07..b849e24167697 100644 --- a/x-pack/plugins/infra/public/components/loading_overlay_wrapper.tsx +++ b/x-pack/plugins/infra/public/components/loading_overlay_wrapper.tsx @@ -8,7 +8,7 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; export const LoadingOverlayWrapper: React.FC< React.HTMLAttributes & { diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 3d69b6a022987..b485a21221af2 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { LogEntryCursor } from '../../../common/log_entry'; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index d579432c6291f..bda52d9323eb6 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -2,7 +2,7 @@ import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks'; import { Subject } from 'rxjs'; import { I18nProvider } from '@kbn/i18n/react'; -import { EuiThemeProvider } from '../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { DEFAULT_SOURCE_CONFIGURATION } from '../../test_utils/source_configuration'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.stories.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.stories.tsx index 7caf75417091a..d6c993b28514b 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.stories.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.stories.tsx @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { QualityWarning } from '../../../../common/log_analysis'; import { CategoryQualityWarnings } from './quality_warning_notices'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx index 4bf618923a138..6503dc6110591 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { groupBy } from 'lodash'; import React, { Fragment, useState } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { CategoryQualityWarning, CategoryQualityWarningReason, diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.stories.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.stories.tsx index 79b3b9ce9bc6f..932b499430aa9 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.stories.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.stories.tsx @@ -7,7 +7,7 @@ import { actions } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { InitialConfigurationStep } from './initial_configuration_step'; storiesOf('infra/logAnalysis/SetupInitialConfigurationStep', module) diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx index 3aa8b544b7b54..04414d43bfde4 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx @@ -6,7 +6,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { missingMlPrivilegesTitle, missingMlResultsPrivilegesDescription, diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx index 6a5a1da890418..2535192681b8a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx @@ -6,7 +6,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { missingMlPrivilegesTitle, missingMlSetupPrivilegesDescription, diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/ml_unavailable_prompt.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/ml_unavailable_prompt.tsx index 4b15ce19ef096..f711e5100391d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/ml_unavailable_prompt.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/ml_unavailable_prompt.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; export const MlUnavailablePrompt: React.FunctionComponent<{}> = () => ( = ({ children, diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_status_unknown_prompt.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_status_unknown_prompt.tsx index 334aaa251f524..615c78080c7ae 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_status_unknown_prompt.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_status_unknown_prompt.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; interface Props { retry: () => void; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx index 366d31897fe06..660f416e3b7b4 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -24,7 +24,7 @@ import { HttpStart } from 'src/core/public'; import { LoadingPage } from '../../loading_page'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrialStatus } from '../../../hooks/use_trial_status'; export const SubscriptionSplashContent: React.FC = () => { diff --git a/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx index 84074568bcfef..5433c435d0647 100644 --- a/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import * as React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; interface LogCustomizationMenuState { isShown: boolean; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx index 2d15068e51da5..e8ffd408087d0 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntryExampleMessagesEmptyIndicator } from './log_entry_examples_empty_indicator'; import { LogEntryExampleMessagesFailureIndicator } from './log_entry_examples_failure_indicator'; import { LogEntryExampleMessagesLoadingIndicator } from './log_entry_examples_loading_indicator'; diff --git a/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx index 7beead461cb2e..00e9c412ddd3e 100644 --- a/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx @@ -18,7 +18,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { useVisibilityState } from '../../utils/use_visibility_state'; interface LogHighlightsMenuProps { diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx index 0528d59f0b3d5..311e25e7c6639 100644 --- a/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx @@ -9,7 +9,7 @@ import { area, curveMonotoneY } from 'd3-shape'; import { max } from 'lodash'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntriesSummaryBucket } from '../../../../common/http_api'; interface DensityChartProps { diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx index 2869f8d0087c2..53d9985c20dc1 100644 --- a/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; interface HighlightedIntervalProps { className?: string; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx index 496d4ebf924a3..a53ce9e3fabc6 100644 --- a/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx @@ -7,7 +7,7 @@ import { scaleLinear } from 'd3-scale'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntryTime } from '../../../../common/log_entry'; import { DensityChart } from './density_chart'; import { HighlightedInterval } from './highlighted_interval'; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx index 6271627589394..6c63654eef804 100644 --- a/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import * as React from 'react'; -import { euiStyled, keyframes } from '../../../../../observability/public'; +import { euiStyled, keyframes } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntryTime } from '../../../../common/log_entry'; import { SearchMarkerTooltip } from './search_marker_tooltip'; import { LogEntriesSummaryHighlightsBucket } from '../../../../common/http_api'; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx index 458fdbfcd4916..8424e798128e2 100644 --- a/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx @@ -7,7 +7,7 @@ import { scaleTime } from 'd3-scale'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { getTimeLabelFormat } from './time_label_formatter'; interface TimeRulerProps { diff --git a/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx index 248dce8f6bf8c..4c5237d1c9030 100644 --- a/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; interface LogSearchInputProps { className?: string; diff --git a/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx b/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx index 64dda6ce74d89..f0d062270f01b 100644 --- a/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; export const LogStatusbar = euiStyled(EuiFlexGroup).attrs(() => ({ alignItems: 'center', diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index a38df73837416..23c3e9c4391e4 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { transparentize } from 'polished'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntryColumn, LogEntryColumnContent, diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx index b13e3569c1e5b..884e26359ba45 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx @@ -6,7 +6,7 @@ import stringify from 'json-stable-stringify'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { JsonArray, JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/highlighting.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/highlighting.tsx index 2af99e30ce44f..159ed6d427926 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/highlighting.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/highlighting.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { chooseLightOrDarkColor, tintOrShade } from '../../../utils/styles'; export const ActiveHighlightMarker = euiStyled.mark` diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/jump_to_tail.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/jump_to_tail.tsx index 50c26784bbdab..17b11205e806d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/jump_to_tail.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/jump_to_tail.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; interface LogTextStreamJumpToTailProps { onClickJump?: () => void; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx index 1dd6e0b23e6bc..f176bad9e7a17 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx @@ -16,7 +16,7 @@ import { FormattedMessage, FormattedTime, FormattedRelative } from '@kbn/i18n/re import * as React from 'react'; import { Unit } from '@elastic/datemath'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogTextSeparator } from './log_text_separator'; import { extendDatemath } from '../../../utils/datemath'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx index a82c2869baa93..d0f2fa7be6f96 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx @@ -6,7 +6,7 @@ import { useMemo } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TextScale } from '../../../../common/log_text_scale'; import { LogColumnRenderConfiguration, diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx index 8582be008a44a..d2cb3baef350d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx @@ -14,7 +14,7 @@ import { EuiContextMenuItem, } from '@elastic/eui'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogEntryColumnContent } from './log_entry_column'; interface LogEntryContextMenuItem { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx index 5813f08497a74..8de9e565b00be 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx @@ -6,7 +6,7 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { LogFieldColumn } from '../../../../common/http_api'; import { LogEntryFieldColumn } from './log_entry_field_column'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index fe5e7f305f60c..4a9b0d0906a76 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogColumn } from '../../../../common/http_api'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; import { FieldValue } from './field_value'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx index b9871cc3b36f4..5d36e5cd47c59 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx @@ -6,7 +6,7 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { LogMessageColumn } from '../../../../common/http_api'; import { LogEntryMessageColumn } from './log_entry_message_column'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx index 65e7b2bf2273d..bfc160ada2e6a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx @@ -5,7 +5,7 @@ */ import React, { memo, useMemo } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogColumn, LogMessagePart } from '../../../../common/http_api'; import { isConstantSegment, diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 5a653300217d7..93c657fbdda97 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -8,7 +8,8 @@ import React, { memo, useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { euiStyled, useUiTracker } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useUiTracker } from '../../../../../observability/public'; import { isTimestampColumn } from '../../../utils/log_entry'; import { TextScale } from '../../../../common/log_text_scale'; import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx index 21568974463eb..d13fb4b6d7bb6 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx @@ -6,7 +6,7 @@ import React, { memo } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TimeFormat, useFormattedTime } from '../../formatted_time'; import { LogEntryColumnContent } from './log_entry_column'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index 3c86ce3e32526..d399e47a73562 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import moment from 'moment'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TextScale } from '../../../../common/log_text_scale'; import { TimeKey, UniqueTimeKey } from '../../../../common/time'; import { callWithoutRepeats } from '../../../utils/handlers'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 7fc97f949f068..d7b94d4c02912 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useState, useCallback } from 'react'; -import { euiStyled, css } from '../../../../../observability/public'; +import { euiStyled, css } from '../../../../../../../src/plugins/kibana_react/common'; import { TextScale } from '../../../../common/log_text_scale'; export type WrapMode = 'none' | 'pre-wrapped' | 'long'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx index 9b8fecc7fb111..a4fed85a6a88e 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx @@ -8,7 +8,7 @@ import { bisector } from 'd3-array'; import { sortBy, throttle } from 'lodash'; import * as React from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Rect } from './measurable_item_view'; interface VerticalScrollPanelProps { diff --git a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx index 9da892ec92ec1..68d523f5066a1 100644 --- a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx +++ b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; interface AppNavigationProps { 'aria-label': string; diff --git a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx index d9340d804fb24..1db4e0ee70883 100644 --- a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx +++ b/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx @@ -8,7 +8,7 @@ import { EuiLink, EuiTab, EuiTabs } from '@elastic/eui'; import React from 'react'; import { Route } from 'react-router-dom'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { useLinkProps } from '../../hooks/use_link_props'; import { LinkDescriptor } from '../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/components/page.tsx b/x-pack/plugins/infra/public/components/page.tsx index 9636a5fc3a631..a76500a1d723f 100644 --- a/x-pack/plugins/infra/public/components/page.tsx +++ b/x-pack/plugins/infra/public/components/page.tsx @@ -6,7 +6,7 @@ import { EuiPage } from '@elastic/eui'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; export const ColumnarPage = euiStyled.div` display: flex; diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts index 686b563068d60..a0039f6370f1f 100644 --- a/x-pack/plugins/infra/public/components/toolbar_panel.ts +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -5,7 +5,7 @@ */ import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ grow: false, diff --git a/x-pack/plugins/infra/public/pages/error.tsx b/x-pack/plugins/infra/public/pages/error.tsx index c34af31ce28a7..55892c424f465 100644 --- a/x-pack/plugins/infra/public/pages/error.tsx +++ b/x-pack/plugins/infra/public/pages/error.tsx @@ -15,7 +15,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../observability/public'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { Header } from '../components/header'; import { ColumnarPage, PageContent } from '../components/page'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 5c1e8f2bdcfcc..ecddd8a9aa5be 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -10,7 +10,8 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled, useTrackPageview } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { CategoryJobNoticesSection } from '../../../components/logging/log_analysis_job_status'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx index be8281ce54556..d5480977e7f9e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { memo } from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; export const RegularExpressionRepresentation: React.FunctionComponent<{ maximumSegmentCount?: number; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx index 748c82cc5cd5a..779ac3e8c3a07 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_comparison.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_comparison.tsx index d73f9f33fe5db..e4da367721c2d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_comparison.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_comparison.tsx @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; export const SingleMetricComparison: React.FunctionComponent<{ currentValue: number; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx index 96abe4ab42669..954b6a9ab3ed3 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo, useCallback } from 'react'; import useSet from 'react-use/lib/useSet'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { LogEntryCategory, LogEntryCategoryDataset, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index c4a464a4cffad..09d3746c6ace6 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -11,7 +11,8 @@ import { stringify } from 'query-string'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { encode, RisonValue } from 'rison-node'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled, useTrackPageview } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; import { TimeKey } from '../../../../common/time'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index e6489417f2d07..37032a95e9640 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React from 'react'; import useMount from 'react-use/lib/useMount'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { LogEntryAnomaly } from '../../../../../../common/http_api'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index ff9b9db2e5087..c89f0329e9f2e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index b639cecf676ad..ab3476cd78eb3 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback, useState } from 'react'; import moment from 'moment'; import { encode } from 'rison-node'; import { i18n } from '@kbn/i18n'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; import { LogEntryColumn, diff --git a/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx index 551d200f1895d..887e706c909ba 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx @@ -15,7 +15,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogColumnConfiguration } from '../../../utils/source_configuration'; import { useVisibilityState } from '../../../utils/use_visibility_state'; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 05bcd4d0984e8..02125807faf54 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -5,7 +5,7 @@ */ import React, { useContext, useCallback } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { AutoSizer } from '../../../components/auto_sizer'; import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; import { LogMinimap } from '../../../components/logging/log_minimap'; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index 8a4081288b283..3fa89da5b5e51 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -19,7 +19,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { LogEntry } from '../../../../common/http_api'; import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; import { useViewportDimensions } from '../../../utils/use_viewport_dimensions'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogStream } from '../../../components/log_stream'; const MODAL_MARGIN = 25; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 5c6e124914f39..6055b60719a68 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -7,8 +7,8 @@ import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; - -import { euiStyled, useUiTracker } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { useUiTracker } from '../../../../../../observability/public'; import { InfraFormatter } from '../../../../lib/lib'; import { Timeline } from './timeline/timeline'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx index 62b25d5a36870..b574347970df6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import { withTheme, EuiTheme } from '../../../../../../observability/public'; +import { withTheme, EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; interface Props { 'data-test-subj'?: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 2f312d9ee64ac..f8b0bbf62d2b5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -19,7 +19,7 @@ import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; import { useSourceContext } from '../../../../containers/source'; import { InfraFormatterType } from '../../../../lib/lib'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; import { IntervalLabel } from './waffle/interval_label'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx index f07c37f5e7ea2..4e18880f1fd22 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { LoadingPage } from '../../../../../../components/loading_page'; import { useTrialStatus } from '../../../../../../hooks/use_trial_status'; import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { HttpStart } from '../../../../../../../../../../src/core/public'; export const SubscriptionSplashContent: React.FC = () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 661844a627a58..a053e2a72cea0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -10,7 +10,7 @@ import React, { useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { EuiOutsideClickDetector } from '@elastic/eui'; import { EuiIcon, EuiButtonIcon } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { MetricsTab } from './tabs/metrics/metrics'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index 3f0798c4a1670..bc03f9337813f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -21,7 +21,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { AutoSizer } from '../../../../../../../components/auto_sizer'; -import { euiStyled } from '../../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Process } from './types'; import { ProcessRowCharts } from './process_row_charts'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx index af515ae75854c..a3f75402df2ca 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx @@ -25,7 +25,7 @@ import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/ import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; import { Color } from '../../../../../../../../common/color_palette'; -import { euiStyled } from '../../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { useProcessListRowChart } from '../../../../hooks/use_process_list_row_chart'; import { Process } from './types'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 1ea6e397e7768..f420fd09e6e68 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -25,7 +25,7 @@ import { } from '@elastic/eui'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { FORMATTERS } from '../../../../../../../../common/formatters'; -import { euiStyled } from '../../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { SortBy } from '../../../../hooks/use_process_list'; import { Process } from './types'; import { ProcessRow } from './process_row'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index 5bbba906b62f2..93d98853539b9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -16,7 +16,7 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { STATE_NAMES } from './states'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx index 46b63bc400a2b..f06cdd3cb0875 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx @@ -15,7 +15,7 @@ import { useMetadata } from '../../../../../metric_detail/hooks/use_metadata'; import { getFields } from './build_fields'; import { useWaffleTimeContext } from '../../../../hooks/use_waffle_time'; import { Table } from './table'; -import { euiStyled } from '../../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { useWaffleFiltersContext } from '../../../../hooks/use_waffle_filters'; const TabComponent = (props: TabProps) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx index 6ff31e86c9d5e..670fc3673de25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx @@ -6,7 +6,7 @@ import { InventoryItemType } from '../../../../../../../common/inventory_models/types'; import { InfraWaffleMapOptions, InfraWaffleMapNode } from '../../../../../../lib/lib'; -import { euiStyled } from '../../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; export interface TabProps { options: InfraWaffleMapOptions; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index 9b6853dcdc751..de31e690e3659 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { getBreakpoint } from '@elastic/eui'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib'; import { NoData } from '../../../../components/empty_states'; import { InfraLoadingPanel } from '../../../../components/loading'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index f1e796ef8ba18..44993f688ebc2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -38,7 +38,7 @@ import { MetricsExplorerChartType } from '../../../metrics_explorer/hooks/use_me import { getTimelineChartTheme } from '../../../metrics_explorer/components/helpers/get_chart_theme'; import { calculateDomain } from '../../../metrics_explorer/components/helpers/calculate_domain'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraFormatter } from '../../../../../lib/lib'; import { useMetricsHostsAnomaliesResults } from '../../hooks/use_metrics_hosts_anomalies'; import { useMetricsK8sAnomaliesResults } from '../../hooks/use_metrics_k8s_anomalies'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index fbca85e2d4496..c69bdb7c4379e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; // import { act } from 'react-dom/test-utils'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { EuiToolTip } from '@elastic/eui'; import { ConditionalToolTip } from './conditional_tooltip'; import { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index 7ec1ae905a640..a559882987bfa 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -8,7 +8,7 @@ import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { first } from 'lodash'; import { getCustomMetricLabel } from '../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../common/http_api'; -import { withTheme, EuiTheme } from '../../../../../../../observability/public'; +import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { useSourceContext } from '../../../../../containers/source'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx index 953d94e51aad0..a900c46376ba6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraFormatter, InfraWaffleMapBounds, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx index 75a023d49d4aa..1e592b846c320 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx @@ -6,7 +6,7 @@ import { EuiLink, EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapGroup, InfraWaffleMapOptions } from '../../../../../lib/lib'; interface Props { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx index 760dae169d000..6741cd0f851ea 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfGroups, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx index 5fcee6193b357..056fc4dcb79fe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfNodes, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index ea7bb66e689d9..202d263084896 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback } from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraFormatter, InfraWaffleMapBounds, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index f4da68d9dead7..13fa3c86fa9aa 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react'; import { first, last } from 'lodash'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib'; import { WaffleLegendOptions } from '../../hooks/use_waffle_options'; import { getColorPalette } from '../../lib/get_color_palette'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index 8023e3bf7da62..147876d3b377d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { nodesToWaffleMap } from '../../lib/nodes_to_wafflemap'; import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from '../../lib/type_guards'; import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../../../../lib/lib'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index 262d94d8f3674..c1c923092726a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -27,7 +27,7 @@ import { SNAPSHOT_CUSTOM_AGGREGATIONS, SnapshotCustomAggregationRT, } from '../../../../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; +import { EuiTheme, withTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; interface SelectedOption { label: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index 831a0cde49cfb..2c37049a9a6f5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -8,7 +8,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; +import { EuiTheme, withTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index 956241545e8be..c2076dca6d581 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CustomMetricMode } from './types'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; +import { EuiTheme, withTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index 4024f6b505c29..11d1b60034c78 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { first } from 'lodash'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InfraWaffleMapNode, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 3179d4aa05268..0d0333ca27c64 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -16,6 +16,7 @@ import { createUptimeLink } from '../../lib/create_uptime_link'; import { findInventoryModel, findInventoryFields } from '../../../../../../common/inventory_models'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { Section, SectionLinkProps, @@ -24,8 +25,6 @@ import { SectionSubtitle, SectionLinks, SectionLink, - withTheme, - EuiTheme, } from '../../../../../../../observability/public'; import { useLinkProps } from '../../../../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx index 09e323b55a71a..1d8fcb20a261c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InventoryColorPalette } from '../../../../../lib/lib'; import { getColorPalette } from '../../lib/get_color_palette'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx index fadf6b139a3e8..ed34a32012bd2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InfraFormatter, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx index 1ef0f2d0c4288..3ef76c6a70733 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx @@ -7,7 +7,7 @@ import { darken } from 'polished'; import React from 'react'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraFormatter, InfraWaffleMapRuleOperator, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 8d2f289621b12..349f9a9b25e9b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; import { InfraGroupByOptions } from '../../../../../lib/lib'; import { CustomFieldPanel } from './custom_field_panel'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; import { DropdownButton } from '../dropdown_button'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx index a45ac0cee72d9..55466049bc71a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo, useState, ReactNode } from 'react'; import { EuiSwitch, EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiTheme, withTheme } from '../../../../../../../observability/public'; +import { EuiTheme, withTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { WaffleSortOption } from '../../hooks/use_waffle_options'; import { DropdownButton } from '../dropdown_button'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx index da044b1cf99ee..01e938b5e6a62 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiDatePicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useCallback } from 'react'; -import { withTheme, EuiTheme } from '../../../../../../../observability/public'; +import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; interface Props { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx index 2d06713f7fe1a..be73c8a0b5e1c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx @@ -16,7 +16,7 @@ import { import { get, last, max } from 'lodash'; import React, { ReactText } from 'react'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { createFormatter } from '../../../../../common/formatters'; import { InventoryFormatterType } from '../../../../../common/inventory_models/types'; import { SeriesOverrides, VisSectionProps } from '../types'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index b9b8e83b598f3..e81dedba64043 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { ViewSourceConfigurationButton } from '../../../../components/source_configuration'; import { useLinkProps } from '../../../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx index 4620102517549..e33785632555e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx @@ -5,7 +5,7 @@ */ import { EuiPageContent } from '@elastic/eui'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; export const LayoutContent = euiStyled(EuiPageContent)` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx index 656378fbc0610..953659731407c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx @@ -9,7 +9,7 @@ import { EuiButtonIcon, EuiFlexGrid, EuiFlexItem, EuiTitle, EuiText } from '@ela import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { InfraMetadata } from '../../../../../common/http_api'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { MetadataContext } from '../containers/metadata_context'; interface FieldDef { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx index 0d0bc8c82397e..a30b2c17d3517 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx @@ -21,7 +21,7 @@ import { AutoSizer } from '../../../../components/auto_sizer'; import { MetricsTimeControls } from './time_controls'; import { SideNavContext, NavItem } from '../lib/side_nav_context'; import { PageBody } from './page_body'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { InfraMetadata } from '../../../../../common/http_api/metadata_api'; import { PageError } from './page_error'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx index 1cba3366acbbb..e1fb307d35f5f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx @@ -6,7 +6,7 @@ import { EuiHideFor, EuiPageSideBar, EuiShowFor, EuiSideNav } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { NavItem } from '../lib/side_nav_context'; interface Props { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx index afee0c0498187..ba9e71021b232 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx @@ -7,7 +7,7 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 60c8041fb5ef0..4220303d02cf4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -5,7 +5,11 @@ */ import { i18n } from '@kbn/i18n'; import React, { useContext, useState } from 'react'; -import { euiStyled, EuiTheme, withTheme } from '../../../../../observability/public'; +import { + euiStyled, + EuiTheme, + withTheme, +} from '../../../../../../../src/plugins/kibana_react/common'; import { DocumentTitle } from '../../../components/document_title'; import { Header } from '../../../components/header'; import { ColumnarPage, PageContent } from '../../../components/page'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts index 3ec57e23a425d..2331d69d460d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts @@ -5,7 +5,7 @@ */ import rt from 'io-ts'; -import { EuiTheme } from '../../../../../observability/public'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { InventoryFormatterTypeRT } from '../../../../common/inventory_models/types'; import { MetricsTimeInput } from './hooks/use_metrics_time'; import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 41d8014b4a5c1..c228f09cbb645 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -25,7 +25,7 @@ import { MetricsExplorerYAxisMode, MetricsExplorerChartOptions, } from '../hooks/use_metrics_explorer_options'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { createFormatterForMetric } from './helpers/create_formatter_for_metric'; import { MetricExplorerSeriesChart } from './series_chart'; import { MetricsExplorerChartContextMenu } from './chart_context_menu'; diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index ea84a417c20eb..d4dfd265ecda6 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { KibanaContextProvider, RedirectAppLinks, } from '../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../xpack_legacy/common'; import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index 208c840b403e9..b16a560899b52 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -8,10 +8,10 @@ import React, { ComponentType } from 'react'; import { IntlProvider } from 'react-intl'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; import { CoreVitalItem } from '../core_vital_item'; import { LCP_HELP_LABEL, LCP_LABEL } from '../translations'; -import { EuiThemeProvider } from '../../../../typings'; const KibanaReactContext = createKibanaReactContext(({ uiSettings: { get: () => {}, get$: () => new Observable() }, diff --git a/x-pack/plugins/observability/public/hooks/use_theme.tsx b/x-pack/plugins/observability/public/hooks/use_theme.tsx index 51a1ad5029538..ccb8bf6d86be3 100644 --- a/x-pack/plugins/observability/public/hooks/use_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_theme.tsx @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { EuiTheme } from '../../../xpack_legacy/common'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; export function useTheme() { const theme: EuiTheme = useContext(ThemeContext); diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index a28e34e7d4dcb..14cf141c7cd38 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -9,12 +9,12 @@ import { storiesOf } from '@storybook/react'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; import { ObservabilityPluginSetupDeps } from '../../plugin'; -import { EuiThemeProvider } from '../../typings'; import { OverviewPage } from './'; import { alertsFetchData } from './mock/alerts.mock'; import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock'; diff --git a/x-pack/plugins/observability/public/typings/eui_styled_components.tsx b/x-pack/plugins/observability/public/typings/eui_styled_components.tsx deleted file mode 100644 index 9e547b58bc736..0000000000000 --- a/x-pack/plugins/observability/public/typings/eui_styled_components.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import * as styledComponents from 'styled-components'; -import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components'; - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -export interface EuiTheme { - eui: typeof euiLightVars | typeof euiDarkVars; - darkMode: boolean; -} - -function EuiThemeProvider< - OuterTheme extends styledComponents.DefaultTheme = styledComponents.DefaultTheme ->({ - darkMode = false, - ...otherProps -}: Omit, 'theme'> & { - darkMode?: boolean; -}) { - return ( - ({ - ...outerTheme, - eui: darkMode ? euiDarkVars : euiLightVars, - darkMode, - })} - /> - ); -} - -const { - default: euiStyled, - css, - createGlobalStyle, - keyframes, - withTheme, -} = (styledComponents as unknown) as ThemedStyledComponentsModule; - -export { css, euiStyled, EuiThemeProvider, createGlobalStyle, keyframes, withTheme }; diff --git a/x-pack/plugins/observability/public/typings/index.ts b/x-pack/plugins/observability/public/typings/index.ts index 5cc2c613881df..a54d514fde18f 100644 --- a/x-pack/plugins/observability/public/typings/index.ts +++ b/x-pack/plugins/observability/public/typings/index.ts @@ -5,5 +5,4 @@ */ export * from './eui_draggable'; -export * from './eui_styled_components'; export * from './fetch_overview_data'; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index 10158d7aedd0e..bd48acf2ab991 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -12,7 +12,7 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import translations from '../../../translations/translations/ja-JP.json'; import { PluginContext } from '../context/plugin_context'; import { ObservabilityPluginSetupDeps } from '../plugin'; -import { EuiThemeProvider } from '../typings'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; const appMountParameters = ({ setHeaderActionMenu: () => {} } as unknown) as AppMountParameters; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx index 149d948a53fc4..49cc345a631dd 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx @@ -11,7 +11,7 @@ import { Router } from 'react-router-dom'; import { History } from 'history'; import useObservable from 'react-use/lib/useObservable'; import { Store } from 'redux'; -import { EuiThemeProvider } from '../../../../../xpack_legacy/common'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from '../../components/endpoint/route_capture'; import { StartPlugins } from '../../../types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts index b6f5c9b7421b5..e989077e1b8cb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts @@ -5,7 +5,7 @@ */ import styled from 'styled-components'; -import { EuiTheme } from '../../../../../../xpack_legacy/common'; +import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; type SpacingOptions = keyof EuiTheme['eui']['spacerSizes']; diff --git a/x-pack/plugins/uptime/public/apps/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx index 061398b25e452..13d4e1538dc4a 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -29,7 +29,7 @@ import { UptimeAlertsFlyoutWrapper } from '../components/overview/alerts'; import { store } from '../state'; import { kibanaService } from '../state/kibana_service'; import { ActionMenu } from '../components/common/header/action_menu'; -import { EuiThemeProvider } from '../../../observability/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; export interface UptimeAppColors { danger: string; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx index 2a35587a1960f..be4f0fc62271d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx @@ -20,7 +20,8 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { Ping } from '../../../../../common/runtime_types/ping'; import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; -import { euiStyled, FETCH_STATUS, useFetcher } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { useFetcher, FETCH_STATUS } from '../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../contexts'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 7bf5100730f5e..312014dfb35d7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -6,7 +6,7 @@ import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; -import { euiStyled } from '../../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 44e63f04f7bec..e9694cde61307 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -13,7 +13,7 @@ import { renderLegendItem, renderSidebarItem, } from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { WaterfallChartOuterContainer } from './styles'; describe('waterfall', () => { diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx index 851da79314552..27fdd1f8071bd 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx @@ -8,7 +8,7 @@ import React, { useRef, useEffect, RefObject } from 'react'; import { EuiSuggestItem } from '@elastic/eui'; import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; const SuggestionItem = euiStyled.div<{ selected: boolean }>` background: ${(props) => (props.selected ? props.theme.eui.euiColorLightestShade : 'initial')}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx index 4f8fb712de679..817845307c40a 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash'; import { rgba } from 'polished'; import { Suggestion } from './suggestion'; import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; export const unit = 16; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.test.tsx index 41a74fc0aec5c..e882fc730e420 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getLocationStatus, MonitorListStatusColumn } from './monitor_status_column'; import { Ping } from '../../../../../common/runtime_types'; import { STATUS } from '../../../../../common/constants'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { mockDate, mockMoment } from '../../../../lib/helper/test_helpers'; import { render } from '../../../../lib/helper/rtl_helpers'; import { fireEvent, screen, waitFor } from '@testing-library/react'; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx index 81e486bb467bb..ce446db17dcd4 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx @@ -19,7 +19,7 @@ import { } from '../../../../../common/constants'; import { UptimeThemeContext } from '../../../../contexts'; -import { euiStyled } from '../../../../../../observability/public'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../../common/translations'; interface MonitorListStatusColumnProps { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx index 82a7100a48081..bec90191d1105 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.test.tsx @@ -19,7 +19,7 @@ import * as redux from 'react-redux'; import moment from 'moment'; import { IHttpFetchError } from '../../../../../../../src/core/public'; import { mockMoment } from '../../../lib/helper/test_helpers'; -import { EuiThemeProvider } from '../../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index 97bd63614141b..78f553121a3fd 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -13,7 +13,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from 'src/core/public/mocks'; import { configure } from '@testing-library/dom'; import { mockState } from '../__mocks__/uptime_store.mock'; -import { EuiThemeProvider } from '../../../../observability/public'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, KibanaServices, diff --git a/x-pack/plugins/xpack_legacy/common/index.ts b/x-pack/plugins/xpack_legacy/common/index.ts deleted file mode 100644 index 8c0dace27faf4..0000000000000 --- a/x-pack/plugins/xpack_legacy/common/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './eui_styled_components'; From 474af9f3ebcf506a3f2076a1491c250bc5e0810f Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 25 Jan 2021 11:56:42 -0800 Subject: [PATCH 51/60] [Enterprise Search] Add Kea test helper for directly accessing listeners (#89061) * Add getListeners to Kea test helpers * Update TelemetryLogic to use new getListeners helper Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/applications/__mocks__/kea.mock.ts | 25 +++++++++++++++++++ .../shared/telemetry/telemetry_logic.test.ts | 16 ++++-------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index 78ffbcfa3526f..5ac8cce04181e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -120,4 +120,29 @@ export class LogicMounter { public unmount = () => { this.unmountFn(); }; + + /** + * Some tests (e.g. async tests, tests that expect thrown errors) need to access + * listener functions directly instead of calling `SomeLogic.actions.someListener`, + * due to how Kea invokes/wraps action fns by design. + * + * Example usage: + * + * const { mount, getListeners } = new LogicMounter(SomeLogic); + * + * it('some test', async () => { + * mount(); + * const { someListener } = getListeners({ values: { someMockValue: false } }); + * + * const mockBreakpoint = jest.fn(); + * await someListener({ someMockArgument: true }, mockBreakpoint); + * }); + */ + public getListeners = (listenersArgs: object = {}) => { + const { listeners } = this.logicFile.inputs[0]; + + return typeof listeners === 'function' + ? (listeners as Function)(listenersArgs) // e.g., listeners({ values, actions, props }) => ({ ... }) + : listeners; // handles simpler logic files that just define listeners: { ... } + }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts index 6d4e4f4fe649c..770dcf074f163 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts @@ -4,20 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; - import { JSON_HEADER as headers } from '../../../../common/constants'; -import { mockHttpValues } from '../../__mocks__/http_logic.mock'; +import { LogicMounter, mockHttpValues } from '../../__mocks__'; -import { TelemetryLogic } from './'; +import { TelemetryLogic } from './telemetry_logic'; describe('Telemetry logic', () => { + const { mount, getListeners } = new LogicMounter(TelemetryLogic); const { http } = mockHttpValues; beforeEach(() => { jest.clearAllMocks(); - resetContext({}); - TelemetryLogic.mount(); + mount(); }); describe('sendTelemetry', () => { @@ -36,11 +34,7 @@ describe('Telemetry logic', () => { it('throws an error if the telemetry endpoint fails', async () => { http.put.mockImplementationOnce(() => Promise.reject()); - - // To capture thrown errors, we have to call the listener fn directly - // instead of using `TelemetryLogic.actions.sendTelemetry` - this is - // due to how Kea invokes/wraps action fns by design. - const { sendTelemetry } = (TelemetryLogic.inputs[0] as any).listeners({ actions: {} }); + const { sendTelemetry } = getListeners(); await expect(sendTelemetry({ action: '', metric: '', product: '' })).rejects.toThrow( 'Unable to send telemetry' From a31c3eba13f4fb7951a7865f9b2efd4220b30bc8 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 25 Jan 2021 12:24:04 -0800 Subject: [PATCH 52/60] [App Search] Add AnalyticsCards & AnalyticsChart components to analytics pages (#88930) * Create reusable AnalyticsCards component * Update EngineOverview to use new AnalyticsCards component * Update Analytics overview with AnalyticsCards + data * Update QueryDetail with AnalyticsCards + data * Update Analytics overview with AnalyticsChart + data - turns out we do need startDate after all for charts, so I added it back to types * Update QueryDetail with AnalyticsChart + data * [Polish] Dash click and no result lines to match standalone UI Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../analytics/analytics_logic.test.ts | 25 +++++++- .../components/analytics/analytics_logic.ts | 55 ++++++++++++++++ .../components/analytics_cards.test.tsx | 38 ++++++++++++ .../analytics/components/analytics_cards.tsx | 32 ++++++++++ .../components/analytics_chart.test.tsx | 8 +++ .../analytics/components/analytics_chart.tsx | 4 +- .../components/analytics/components/index.ts | 1 + .../components/analytics/constants.ts | 4 ++ .../app_search/components/analytics/index.ts | 2 +- .../app_search/components/analytics/types.ts | 3 +- .../analytics/views/analytics.test.tsx | 15 ++++- .../components/analytics/views/analytics.tsx | 62 ++++++++++++++++++- .../analytics/views/query_detail.test.tsx | 10 +++ .../analytics/views/query_detail.tsx | 35 +++++++++++ .../components/total_stats.test.tsx | 36 ++--------- .../components/total_stats.tsx | 38 ++++++------ 16 files changed, 312 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts index 30a64219403b5..0901ff2737803 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -29,6 +29,15 @@ describe('AnalyticsLogic', () => { dataLoading: true, analyticsUnavailable: false, allTags: [], + totalQueries: 0, + totalQueriesNoResults: 0, + totalClicks: 0, + totalQueriesForQuery: 0, + queriesPerDay: [], + queriesNoResultsPerDay: [], + clicksPerDay: [], + queriesPerDayForQuery: [], + startDate: '', }; const MOCK_TOP_QUERIES = [ @@ -66,6 +75,7 @@ describe('AnalyticsLogic', () => { const MOCK_ANALYTICS_RESPONSE = { analyticsUnavailable: false, allTags: ['some-tag'], + startDate: '1970-01-01', recentQueries: MOCK_RECENT_QUERIES, topQueries: MOCK_TOP_QUERIES, topQueriesNoResults: MOCK_TOP_QUERIES, @@ -81,6 +91,7 @@ describe('AnalyticsLogic', () => { const MOCK_QUERY_RESPONSE = { analyticsUnavailable: false, allTags: ['some-tag'], + startDate: '1970-01-01', totalQueriesForQuery: 50, queriesPerDayForQuery: [25, 0, 25], topClicksForQuery: MOCK_TOP_CLICKS, @@ -120,7 +131,14 @@ describe('AnalyticsLogic', () => { dataLoading: false, analyticsUnavailable: false, allTags: ['some-tag'], - // TODO: more state will get set here in future PRs + startDate: '1970-01-01', + totalClicks: 1000, + totalQueries: 5000, + totalQueriesNoResults: 500, + queriesPerDay: [10, 50, 100], + queriesNoResultsPerDay: [1, 2, 3], + clicksPerDay: [0, 10, 50], + // TODO: Replace this with ...MOCK_ANALYTICS_RESPONSE once all data is set }); }); }); @@ -135,7 +153,10 @@ describe('AnalyticsLogic', () => { dataLoading: false, analyticsUnavailable: false, allTags: ['some-tag'], - // TODO: more state will get set here in future PRs + startDate: '1970-01-01', + totalQueriesForQuery: 50, + queriesPerDayForQuery: [25, 0, 25], + // TODO: Replace this with ...MOCK_QUERY_RESPONSE once all data is set }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts index 4d8603f4bb6d6..537de02a0fee5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts @@ -62,6 +62,61 @@ export const AnalyticsLogic = kea allTags, }, ], + totalQueries: [ + 0, + { + onAnalyticsDataLoad: (_, { totalQueries }) => totalQueries, + }, + ], + totalQueriesNoResults: [ + 0, + { + onAnalyticsDataLoad: (_, { totalQueriesNoResults }) => totalQueriesNoResults, + }, + ], + totalClicks: [ + 0, + { + onAnalyticsDataLoad: (_, { totalClicks }) => totalClicks, + }, + ], + queriesPerDay: [ + [], + { + onAnalyticsDataLoad: (_, { queriesPerDay }) => queriesPerDay, + }, + ], + queriesNoResultsPerDay: [ + [], + { + onAnalyticsDataLoad: (_, { queriesNoResultsPerDay }) => queriesNoResultsPerDay, + }, + ], + clicksPerDay: [ + [], + { + onAnalyticsDataLoad: (_, { clicksPerDay }) => clicksPerDay, + }, + ], + totalQueriesForQuery: [ + 0, + { + onQueryDataLoad: (_, { totalQueriesForQuery }) => totalQueriesForQuery, + }, + ], + queriesPerDayForQuery: [ + [], + { + onQueryDataLoad: (_, { queriesPerDayForQuery }) => queriesPerDayForQuery, + }, + ], + startDate: [ + '', + { + onAnalyticsDataLoad: (_, { startDate }) => startDate, + onQueryDataLoad: (_, { startDate }) => startDate, + }, + ], }), listeners: ({ actions }) => ({ loadAnalyticsData: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx new file mode 100644 index 0000000000000..eee6517d5b480 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiStat } from '@elastic/eui'; + +import { AnalyticsCards } from './'; + +describe('AnalyticsCards', () => { + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiStat)).toHaveLength(2); + expect(wrapper.find('[data-test-subj="RedFish"]').prop('title')).toEqual(100); + expect(wrapper.find('[data-test-subj="RedFish"]').prop('description')).toEqual('Red fish'); + expect(wrapper.find('[data-test-subj="BlueFish"]').prop('title')).toEqual(2000); + expect(wrapper.find('[data-test-subj="BlueFish"]').prop('description')).toEqual('Blue fish'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx new file mode 100644 index 0000000000000..0359283059171 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; + +interface Props { + stats: Array<{ + text: string; + stat: number; + dataTestSubj?: string; + }>; +} +export const AnalyticsCards: React.FC = ({ stats }) => ( + + {stats.map(({ text, stat, dataTestSubj }) => ( + + + + + + ))} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx index 4e071ac7982bd..b2e3e615e4817 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx @@ -54,6 +54,14 @@ describe('AnalyticsChart', () => { expect(wrapper.find(LineSeries)).toHaveLength(3); }); + it('renders dashed lines', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(LineSeries).prop('lineSeriesStyle')?.line?.dash).toBeTruthy(); + }); + it('formats x-axis dates correctly', () => { const wrapper = shallow(); const dateFormatter: Function = wrapper.find('#bottom-axis').prop('tickFormat'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx index 02ad2dff92827..a73d3c9054ed6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx @@ -25,6 +25,7 @@ interface Props { lines: Array<{ id: string; data: ChartData; + isDashed?: boolean; }>; } export const AnalyticsChart: React.FC = ({ height = 300, lines }) => { @@ -39,7 +40,7 @@ export const AnalyticsChart: React.FC = ({ height = 300, lines }) => { headerFormatter: (tooltip) => moment(tooltip.value).format(TOOLTIP_DATE_FORMAT), }} /> - {lines.map(({ id, data }) => ( + {lines.map(({ id, data, isDashed }) => ( = ({ height = 300, lines }) => { xAccessor={'x'} yAccessors={['y']} curve={CurveType.CURVE_MONOTONE_X} + lineSeriesStyle={isDashed ? { line: { dash: [5, 5] } } : undefined} /> ))} { it('renders', () => { + setMockValues({ + totalQueries: 3, + totalQueriesNoResults: 2, + totalClicks: 1, + queriesPerDay: [10, 20, 30], + queriesNoResultsPerDay: [1, 2, 3], + clicksPerDay: [0, 1, 5], + startDate: '1970-01-01', + }); const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(AnalyticsCards)).toHaveLength(1); + expect(wrapper.find(AnalyticsChart)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx index 5febeae203aba..d3c3bff5a2947 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx @@ -5,13 +5,73 @@ */ import React from 'react'; +import { useValues } from 'kea'; -import { ANALYTICS_TITLE } from '../constants'; +import { EuiSpacer } from '@elastic/eui'; + +import { + ANALYTICS_TITLE, + TOTAL_QUERIES, + TOTAL_QUERIES_NO_RESULTS, + TOTAL_CLICKS, +} from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; export const Analytics: React.FC = () => { + const { + totalQueries, + totalQueriesNoResults, + totalClicks, + queriesPerDay, + queriesNoResultsPerDay, + clicksPerDay, + startDate, + } = useValues(AnalyticsLogic); + return ( + + + + + +

TODO: Analytics overview

); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx index 2e4bd36d79382..99485340f6b88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx @@ -5,6 +5,7 @@ */ import '../../../../__mocks__/react_router_history.mock'; +import { setMockValues } from '../../../../__mocks__'; import React from 'react'; import { useParams } from 'react-router-dom'; @@ -12,6 +13,7 @@ import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { AnalyticsCards, AnalyticsChart } from '../components'; import { QueryDetail } from './'; describe('QueryDetail', () => { @@ -19,6 +21,11 @@ describe('QueryDetail', () => { beforeEach(() => { (useParams as jest.Mock).mockReturnValueOnce({ query: 'some-query' }); + + setMockValues({ + totalQueriesForQuery: 100, + queriesPerDayForQuery: [0, 5, 10], + }); }); it('renders', () => { @@ -31,5 +38,8 @@ describe('QueryDetail', () => { 'Query', 'some-query', ]); + + expect(wrapper.find(AnalyticsCards)).toHaveLength(1); + expect(wrapper.find(AnalyticsChart)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx index b3b7e5258c536..53c1dc8b845b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx @@ -6,12 +6,16 @@ import React from 'react'; import { useParams } from 'react-router-dom'; +import { useValues } from 'kea'; + import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; const QUERY_DETAIL_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.title', @@ -24,10 +28,41 @@ interface Props { export const QueryDetail: React.FC = ({ breadcrumbs }) => { const { query } = useParams() as { query: string }; + const { totalQueriesForQuery, queriesPerDayForQuery, startDate } = useValues(AnalyticsLogic); + return ( + + + + + +

TODO: Query detail page

); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx index 6cb47e8b419f3..2464eb258452e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx @@ -7,45 +7,19 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiStat } from '@elastic/eui'; +import { shallow } from 'enzyme'; +import { AnalyticsCards } from '../../analytics'; import { TotalStats } from './total_stats'; describe('TotalStats', () => { - let wrapper: ShallowWrapper; - - beforeAll(() => { - jest.clearAllMocks(); + it('renders', () => { setMockValues({ totalQueries: 11, documentCount: 22, totalClicks: 33, }); - wrapper = shallow(); - }); - - it('renders the total queries stat', () => { - expect(wrapper.find('[data-test-subj="TotalQueriesCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(0); - expect(card.prop('title')).toEqual(11); - expect(card.prop('description')).toEqual('Total queries'); - }); - - it('renders the total documents stat', () => { - expect(wrapper.find('[data-test-subj="TotalDocumentsCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(1); - expect(card.prop('title')).toEqual(22); - expect(card.prop('description')).toEqual('Total documents'); - }); - - it('renders the total clicks stat', () => { - expect(wrapper.find('[data-test-subj="TotalClicksCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(2); - expect(card.prop('title')).toEqual(33); - expect(card.prop('description')).toEqual('Total clicks'); + const wrapper = shallow(); + expect(wrapper.find(AnalyticsCards)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx index a27142938f558..a7f5923498455 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; +import { AnalyticsCards } from '../../analytics'; import { EngineOverviewLogic } from '../'; @@ -16,22 +16,24 @@ export const TotalStats: React.FC = () => { const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic); return ( - - - - - - - - - - - - - - - - - + ); }; From ac06965cb089d058cc1e7de74d39b5dcf3ad9f74 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 25 Jan 2021 14:52:12 -0600 Subject: [PATCH 53/60] [DOCS] Fixes the version file (#89220) --- docs/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index b91af2ab51ebf..eb6f794434f8a 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -7,7 +7,7 @@ :blog-ref: https://www.elastic.co/blog/ :wikipedia: https://en.wikipedia.org/wiki -include::{docs-root}/shared/versions/stack/7.10.asciidoc[] +include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/kibana/kibana :docker-image: docker.elastic.co/kibana/kibana:{version} From 5d68b101067ab80aa36e2a85bfd2b2b6df422e91 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 25 Jan 2021 14:39:20 -0700 Subject: [PATCH 54/60] Upgrade EUI to v31.3.0 (#88881) * Bump EUI to v31.3.0 * jest snapshot updates * Fixed space issue in kbnQueryBar date picker * Removed unecessary space in query bar scss Co-authored-by: miukimiu Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../ui/query_string_input/_query_bar.scss | 3 +- .../__snapshots__/cron_editor.test.tsx.snap | 76 +++++++++++++++++++ .../sample_data_view_data_button.test.js.snap | 1 + .../share_context_menu.test.tsx.snap | 3 + .../extended_template.stories.storyshot | 2 + .../date_format.stories.storyshot | 3 + .../number_format.stories.storyshot | 3 + .../saved_elements_modal.stories.storyshot | 3 + .../__snapshots__/tag.stories.storyshot | 4 +- .../__snapshots__/tag_list.stories.storyshot | 6 +- .../text_style_picker.stories.storyshot | 2 + .../workpad_templates.stories.storyshot | 9 ++- .../extended_template.stories.storyshot | 6 ++ .../extended_template.stories.storyshot | 4 + .../__snapshots__/tools_control.test.tsx.snap | 2 + .../toc_entry_actions_popover.test.tsx.snap | 4 + .../remote_cluster_form.test.js.snap | 1 + .../__snapshots__/index.test.tsx.snap | 1 + .../__snapshots__/cert_status.test.tsx.snap | 2 +- .../__snapshots__/donut_chart.test.tsx.snap | 8 +- yarn.lock | 8 +- 22 files changed, 133 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 24297011ccc63..dac83dacf6fbf 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "31.0.0", + "@elastic/eui": "31.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 3e3982dd58e57..65f652df31d0c 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -73,9 +73,10 @@ // sass-lint:disable-block no-important flex-grow: 0 !important; flex-basis: auto !important; - margin-right: -$euiSizeXS !important; &.kbnQueryBar__datePickerWrapper-isHidden { + // sass-lint:disable-block no-important + margin-right: -$euiSizeXS !important; width: 0; overflow: hidden; max-width: 0; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap b/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap index 9207c6467f6a9..151bd91750daa 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap +++ b/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap @@ -170,6 +170,7 @@ exports[`CronEditor is rendered with a DAY frequency 1`] = `
@@ -139,6 +140,7 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot index 8f0a8b5abd9b5..238fe7c259c6e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot @@ -46,6 +46,7 @@ Array [ aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -118,6 +119,7 @@ Array [ aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -189,6 +191,7 @@ exports[`Storyshots arguments/DateFormat with preset format 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot index 17f158c680a67..2159e49e2bcf1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot @@ -56,6 +56,7 @@ Array [ aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" /> @@ -138,6 +139,7 @@ Array [ aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" /> @@ -219,6 +221,7 @@ exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" /> diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index 900665fc1ddec..b833120f84f39 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -75,6 +75,7 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="search" + size="m" /> @@ -222,6 +223,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="search" + size="m" /> @@ -669,6 +671,7 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="search" + size="m" /> diff --git a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot index 6f88120fb2b84..f21ffcf1a70ea 100644 --- a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot @@ -46,7 +46,7 @@ exports[`Storyshots components/Tags/Tag as badge with color 1`] = ` exports[`Storyshots components/Tags/Tag as health 1`] = `
,
,
@@ -587,6 +588,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index d267ba07078fe..489827246e998 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -38,6 +38,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="search" + size="m" />
@@ -319,7 +320,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -206,6 +207,7 @@ exports[`Storyshots arguments/ContainerStyle extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -547,6 +549,7 @@ exports[`Storyshots arguments/ContainerStyle/components appearance form 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -611,6 +614,7 @@ exports[`Storyshots arguments/ContainerStyle/components appearance form 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -962,6 +966,7 @@ exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -1026,6 +1031,7 @@ exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot index 5d666282f9d0e..05add24cf775c 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot @@ -69,6 +69,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -161,6 +162,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -245,6 +247,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" />
@@ -329,6 +332,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` aria-hidden="true" className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" + size="s" /> diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap index a57fcaa393b89..69faf78524326 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap @@ -110,6 +110,7 @@ exports[`Should render cancel button when drawing 1`] = ` }, ] } + size="m" /> @@ -235,6 +236,7 @@ exports[`renders 1`] = ` }, ] } + size="m" /> `; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index ea37e76bc8494..994e152c2ac4d 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -109,6 +109,7 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` }, ] } + size="m" /> `; @@ -222,6 +223,7 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou }, ] } + size="m" /> `; @@ -336,6 +338,7 @@ exports[`TOCEntryActionsPopover should have "show layer" action when layer is no }, ] } + size="m" /> `; @@ -418,6 +421,7 @@ exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1 }, ] } + size="m" /> `; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 177ef8b3c6cad..5f09193be90c2 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -936,6 +936,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u /> diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index a2a9c30ca4e1c..778916ad2d07a 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -355,6 +355,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "small": "14px", "xSmall": "12px", }, + "euiMarkdownEditorMinHeight": "150px", "euiPageBackgroundColor": "#1a1b20", "euiPaletteColorBlind": Object { "euiColorVis0": Object { diff --git a/x-pack/plugins/uptime/public/components/certificates/__snapshots__/cert_status.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__snapshots__/cert_status.test.tsx.snap index 0a84278c32018..79f58ee76d18f 100644 --- a/x-pack/plugins/uptime/public/components/certificates/__snapshots__/cert_status.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/certificates/__snapshots__/cert_status.test.tsx.snap @@ -7,7 +7,7 @@ exports[`CertStatus renders expected elements for valid props 1`] = ` }
Date: Mon, 25 Jan 2021 16:43:46 -0500 Subject: [PATCH 55/60] [alerts] adds support for index threshold index param string type (#88540) resolves https://github.com/elastic/kibana/issues/68575 The index threshold alert defines an `index` parameter which is typed as `string | string[]`. However the UI for this alert has been typing it as only `string[]`. This PR changes the UI to work with an incoming string value for this parameter. If the parameter is edited in the UI, it will always be set as an array, even if there is only one element. --- .../alert_types/threshold/expression.tsx | 21 ++++++++++++------- .../public/alert_types/threshold/types.ts | 2 +- .../alert_types/threshold/validation.test.ts | 2 +- .../apps/triggers_actions_ui/details.ts | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 35b62d1f73f20..8348a797972ae 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -75,6 +75,12 @@ function isString(value: unknown): value is string { return typeof value === 'string'; } +// normalize the `index` parameter to be a string array +function indexParamToArray(index: string | string[]): string[] { + if (!index) return []; + return isString(index) ? [index] : index; +} + export const IndexThresholdAlertTypeExpression: React.FunctionComponent< AlertTypeParamsExpressionProps > = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, charts, data }) => { @@ -92,6 +98,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< timeWindowUnit, } = alertParams; + const indexArray = indexParamToArray(index); const { http } = useKibana().services; const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); @@ -131,8 +138,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, }); - if (index && index.length > 0) { - const currentEsFields = await getFields(http, index); + if (indexArray.length > 0) { + const currentEsFields = await getFields(http, indexArray); const timeFields = getTimeFieldOptions(currentEsFields); setEsFields(currentEsFields); @@ -170,7 +177,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< defaultMessage="Indices to query" /> } - isInvalid={errors.index.length > 0 && index !== undefined} + isInvalid={errors.index.length > 0 && indexArray.length > 0} error={errors.index} helpText={ 0 && index !== undefined} + isInvalid={errors.index.length > 0 && indexArray.length > 0} noSuggestions={!indexOptions.length} options={indexOptions} data-test-subj="thresholdIndexesComboBox" - selectedOptions={(index || []).map((anIndex: string) => { + selectedOptions={indexArray.map((anIndex: string) => { return { label: anIndex, value: anIndex, @@ -306,12 +313,12 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< description={i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexLabel', { defaultMessage: 'index', })} - value={index && index.length > 0 ? renderIndices(index) : firstFieldOption.text} + value={indexArray.length > 0 ? renderIndices(indexArray) : firstFieldOption.text} isActive={indexPopoverOpen} onClick={() => { setIndexPopoverOpen(true); }} - isInvalid={!(index && index.length > 0 && timeField !== '')} + isInvalid={!(indexArray.length > 0 && timeField !== '')} /> } isOpen={indexPopoverOpen} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts index 4868b92feaeb9..32460c19ebdf0 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts @@ -27,7 +27,7 @@ export interface GroupByType { } export interface IndexThresholdAlertParams extends AlertTypeParams { - index: string[]; + index: string | string[]; timeField?: string; aggType: string; aggField?: string; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts index 1f24a094d0ece..d8f2d24b106f5 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts @@ -32,7 +32,7 @@ describe('expression params validation', () => { }); test('if aggField property is invalid should return proper error message', () => { const initialParams: IndexThresholdAlertParams = { - index: ['test'], + index: 'test', aggType: 'avg', threshold: [], timeWindowSize: 1, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 1ed0b38a238b8..35e2e03969023 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -228,7 +228,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { timeWindowUnit: 'm', groupBy: 'all', threshold: [1000, 5000], - index: ['.kibana_1'], + index: '.kibana_1', timeField: 'alert', }, actions: [ From 2ae2692f2babc2a13165cff0fb69a625acd017cc Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 25 Jan 2021 16:04:34 -0600 Subject: [PATCH 56/60] Add null check for empty process data (#89187) --- .../infra/server/lib/host_details/process_list.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index e9d35f3601634..267245098b17a 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -125,9 +125,12 @@ export const getProcessList = async ( command: bucket.key, }; }); - const { - summary, - } = result.aggregations!.summaryEvent.summary.hits.hits[0]._source.system.process; + + let summary: { [p: string]: number } = {}; + if (result.aggregations!.summaryEvent.summary.hits.hits.length) { + summary = result.aggregations!.summaryEvent.summary.hits.hits[0]._source.system.process + .summary; + } return { processList, From fb536f54286a20766a87a88698ca038716e767cb Mon Sep 17 00:00:00 2001 From: Andrea Del Rio Date: Mon, 25 Jan 2021 14:14:47 -0800 Subject: [PATCH 57/60] Make toolbar_button a shared component (#88386) --- src/plugins/kibana_react/public/index.ts | 1 + .../toolbar_button.test.tsx.snap | 199 ++++++++++++++++++ .../public/toolbar_button/index.ts | 9 + .../toolbar_button}/toolbar_button.scss | 19 +- .../toolbar_button/toolbar_button.test.tsx | 47 +++++ .../public/toolbar_button}/toolbar_button.tsx | 32 ++- .../config_panel/layer_settings.tsx | 2 +- .../workspace_panel/chart_switch.tsx | 2 +- .../change_indexpattern.tsx | 2 +- .../lens/public/shared_components/index.ts | 1 - .../shared_components/toolbar_popover.tsx | 2 +- .../axis_settings_popover.tsx | 3 +- 12 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 src/plugins/kibana_react/public/toolbar_button/__snapshots__/toolbar_button.test.tsx.snap create mode 100644 src/plugins/kibana_react/public/toolbar_button/index.ts rename {x-pack/plugins/lens/public/shared_components => src/plugins/kibana_react/public/toolbar_button}/toolbar_button.scss (81%) create mode 100644 src/plugins/kibana_react/public/toolbar_button/toolbar_button.test.tsx rename {x-pack/plugins/lens/public/shared_components => src/plugins/kibana_react/public/toolbar_button}/toolbar_button.tsx (61%) diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index c99da5e9b36b8..cbb60bba47861 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -15,6 +15,7 @@ export * from './ui_settings'; export * from './field_icon'; export * from './field_button'; export * from './table_list_view'; +export * from './toolbar_button'; export * from './split_panel'; export * from './react_router_navigate'; export { ValidatedDualRange, Value } from './validated_range'; diff --git a/src/plugins/kibana_react/public/toolbar_button/__snapshots__/toolbar_button.test.tsx.snap b/src/plugins/kibana_react/public/toolbar_button/__snapshots__/toolbar_button.test.tsx.snap new file mode 100644 index 0000000000000..294be46398e8a --- /dev/null +++ b/src/plugins/kibana_react/public/toolbar_button/__snapshots__/toolbar_button.test.tsx.snap @@ -0,0 +1,199 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`font weights bold is applied 1`] = ` + +`; + +exports[`font weights normal is applied 1`] = ` + +`; + +exports[`hasArrow is rendered 1`] = ` + +`; + +exports[`positions center is applied 1`] = ` + +`; + +exports[`positions left is applied 1`] = ` + +`; + +exports[`positions none is applied 1`] = ` + +`; + +exports[`positions right is applied 1`] = ` + +`; + +exports[`sizes m is applied 1`] = ` + +`; + +exports[`sizes s is applied 1`] = ` + +`; diff --git a/src/plugins/kibana_react/public/toolbar_button/index.ts b/src/plugins/kibana_react/public/toolbar_button/index.ts new file mode 100644 index 0000000000000..f952741291b68 --- /dev/null +++ b/src/plugins/kibana_react/public/toolbar_button/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './toolbar_button'; diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_button.scss b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.scss similarity index 81% rename from x-pack/plugins/lens/public/shared_components/toolbar_button.scss rename to src/plugins/kibana_react/public/toolbar_button/toolbar_button.scss index 61b02f47678c3..f290b3c7c5f89 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_button.scss +++ b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.scss @@ -1,4 +1,4 @@ -.lnsToolbarButton { +.kbnToolbarButton { line-height: $euiButtonHeight; // Keeps alignment of text and chart icon background-color: $euiColorEmptyShade; @@ -15,11 +15,11 @@ pointer-events: initial; } - .lnsToolbarButton__text > svg { + .kbnToolbarButton__text > svg { margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType` } - .lnsToolbarButton__text:empty { + .kbnToolbarButton__text:empty { margin: 0; } @@ -27,34 +27,33 @@ &[class*='fullWidth'] { text-align: left; - .lnsToolbarButton__content { + .kbnToolbarButton__content { justify-content: space-between; } } - } -.lnsToolbarButton--groupLeft { +.kbnToolbarButton--groupLeft { border-top-right-radius: 0; border-bottom-right-radius: 0; } -.lnsToolbarButton--groupCenter { +.kbnToolbarButton--groupCenter { border-radius: 0; border-left: none; } -.lnsToolbarButton--groupRight { +.kbnToolbarButton--groupRight { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: none; } -.lnsToolbarButton--bold { +.kbnToolbarButton--bold { font-weight: $euiFontWeightBold; } -.lnsToolbarButton--s { +.kbnToolbarButton--s { box-shadow: none !important; // sass-lint:disable-line no-important font-size: $euiFontSizeS; } diff --git a/src/plugins/kibana_react/public/toolbar_button/toolbar_button.test.tsx b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.test.tsx new file mode 100644 index 0000000000000..3d4ce29ffa5e9 --- /dev/null +++ b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.test.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolbarButton, POSITIONS, WEIGHTS, TOOLBAR_BUTTON_SIZES } from './toolbar_button'; + +const noop = () => {}; + +describe('sizes', () => { + TOOLBAR_BUTTON_SIZES.forEach((size) => { + test(`${size} is applied`, () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); + +describe('positions', () => { + POSITIONS.forEach((position) => { + test(`${position} is applied`, () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); + +describe('font weights', () => { + WEIGHTS.forEach((weight) => { + test(`${weight} is applied`, () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); + +describe('hasArrow', () => { + it('is rendered', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_button.tsx b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.tsx similarity index 61% rename from x-pack/plugins/lens/public/shared_components/toolbar_button.tsx rename to src/plugins/kibana_react/public/toolbar_button/toolbar_button.tsx index 2ba227e6ff84f..388a11992268e 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_button.tsx +++ b/src/plugins/kibana_react/public/toolbar_button/toolbar_button.tsx @@ -1,7 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ import './toolbar_button.scss'; @@ -11,16 +13,24 @@ import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui'; const groupPositionToClassMap = { none: null, - left: 'lnsToolbarButton--groupLeft', - center: 'lnsToolbarButton--groupCenter', - right: 'lnsToolbarButton--groupRight', + left: 'toolbarButton--groupLeft', + center: 'toolbarButton--groupCenter', + right: 'toolbarButton--groupRight', }; +type ButtonPositions = keyof typeof groupPositionToClassMap; +export const POSITIONS = Object.keys(groupPositionToClassMap) as ButtonPositions[]; + +type Weights = 'normal' | 'bold'; +export const WEIGHTS = ['normal', 'bold'] as Weights[]; + +export const TOOLBAR_BUTTON_SIZES: Array = ['s', 'm']; + export type ToolbarButtonProps = PropsOf & { /** * Determines prominence */ - fontWeight?: 'normal' | 'bold'; + fontWeight?: Weights; /** * Smaller buttons also remove extra shadow for less prominence */ @@ -32,7 +42,7 @@ export type ToolbarButtonProps = PropsOf & { /** * Adjusts the borders for groupings */ - groupPosition?: 'none' | 'left' | 'center' | 'right'; + groupPosition?: ButtonPositions; dataTestSubj?: string; }; @@ -47,9 +57,9 @@ export const ToolbarButton: React.FunctionComponent = ({ ...rest }) => { const classes = classNames( - 'lnsToolbarButton', + 'kbnToolbarButton', groupPositionToClassMap[groupPosition], - [`lnsToolbarButton--${fontWeight}`, `lnsToolbarButton--${size}`], + [`kbnToolbarButton--${fontWeight}`, `kbnToolbarButton--${size}`], className ); return ( @@ -60,10 +70,10 @@ export const ToolbarButton: React.FunctionComponent = ({ iconType={hasArrow ? 'arrowDown' : ''} color="text" contentProps={{ - className: 'lnsToolbarButton__content', + className: 'kbnToolbarButton__content', }} textProps={{ - className: 'lnsToolbarButton__text', + className: 'kbnToolbarButton__text', }} {...rest} size={size} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx index bc537e5a7d689..d3ca72a02940d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -9,7 +9,7 @@ import { EuiPopover, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; -import { ToolbarButton } from '../../../shared_components'; +import { ToolbarButton } from '../../../../../../../src/plugins/kibana_react/public'; export function LayerSettings({ layerId, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 659626149aef2..474720e638939 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -24,7 +24,7 @@ import { Visualization, FramePublicAPI, Datasource } from '../../../types'; import { Action } from '../state_management'; import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { ToolbarButton } from '../../../shared_components'; +import { ToolbarButton } from '../../../../../../../src/plugins/kibana_react/public'; interface VisualizationSelection { visualizationId: string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 25cb34d19beb8..f2d72079c2185 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui'; import { IndexPatternRef } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { ToolbarButtonProps, ToolbarButton } from '../shared_components'; +import { ToolbarButton, ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { label: string; diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 622bf5397c935..c4e79c3f1bcb8 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -6,6 +6,5 @@ export * from './empty_placeholder'; export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; -export { ToolbarButtonProps, ToolbarButton } from './toolbar_button'; export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index cf2268c6eadf2..adc6d082f514f 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { EuiFlexItem, EuiPopover, EuiIcon, EuiPopoverTitle, IconType } from '@elastic/eui'; -import { ToolbarButton, ToolbarButtonProps } from './toolbar_button'; import { EuiIconLegend } from '../assets/legend'; +import { ToolbarButton, ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; const typeToIconMap: { [type: string]: string | IconType } = { legend: EuiIconLegend as IconType, diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx index 45ec7098aa639..931e62ea1d13f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx @@ -16,12 +16,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LayerConfig, AxesSettingsConfig } from './types'; -import { ToolbarPopover, ToolbarButtonProps } from '../shared_components'; +import { ToolbarPopover } from '../shared_components'; import { isHorizontalChart } from './state_helpers'; import { EuiIconAxisBottom } from '../assets/axis_bottom'; import { EuiIconAxisLeft } from '../assets/axis_left'; import { EuiIconAxisRight } from '../assets/axis_right'; import { EuiIconAxisTop } from '../assets/axis_top'; +import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; export interface AxisSettingsPopoverProps { From 58e629ed7690feb11076a02eff461368cbe8ff32 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 25 Jan 2021 17:30:40 -0500 Subject: [PATCH 58/60] [CI] [TeamCity] Bump security_solution agent size to match other ciGroups, and build missing default plugin (#89241) --- .ci/teamcity/default/build.sh | 1 + .teamcity/src/builds/default/DefaultFunctionalBase.kt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.ci/teamcity/default/build.sh b/.ci/teamcity/default/build.sh index af90e24ef5fe8..140233f29e6af 100755 --- a/.ci/teamcity/default/build.sh +++ b/.ci/teamcity/default/build.sh @@ -14,6 +14,7 @@ node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --scan-dir "$XPACK_DIR/test/usage_collection/plugins" \ --verbose tc_end_block "Build Platform Plugins" diff --git a/.teamcity/src/builds/default/DefaultFunctionalBase.kt b/.teamcity/src/builds/default/DefaultFunctionalBase.kt index d8124bd8521c0..dc2f7756efeb5 100644 --- a/.teamcity/src/builds/default/DefaultFunctionalBase.kt +++ b/.teamcity/src/builds/default/DefaultFunctionalBase.kt @@ -1,6 +1,8 @@ package builds.default +import StandardAgents import addTestSettings +import co.elastic.teamcity.common.requireAgent import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType open class DefaultFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ @@ -8,6 +10,8 @@ open class DefaultFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ param("env.KBN_NP_PLUGINS_BUILT", "true") } + requireAgent(StandardAgents["4"]!!) + dependencies { defaultBuildWithPlugins() } From b93b3a4fc19a3d7c43f3d9b26dc6ce43d1bca73c Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 25 Jan 2021 18:06:09 -0500 Subject: [PATCH 59/60] fix bad link (#89222) --- dev_docs/kibana_platform_plugin_intro.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_docs/kibana_platform_plugin_intro.mdx b/dev_docs/kibana_platform_plugin_intro.mdx index 412b8eb93b00f..3303561fae069 100644 --- a/dev_docs/kibana_platform_plugin_intro.mdx +++ b/dev_docs/kibana_platform_plugin_intro.mdx @@ -76,7 +76,7 @@ We recognize the need to better clarify the relationship between core functional The main difference between core functionality and functionality supplied by plugins, is in how it is accessed. Core is passed to plugins as the first parameter to their `start` and `setup` lifecycle functions, while plugin supplied functionality is passed as the second parameter. Plugin dependencies must be declared explicitly inside the `kibana.json` file. Core functionality is always provided. Read the -section on [how plugins interact with eachother and core](#how-plugins-interact-with-each-other-and-core) for more information. +section on for more information. ## The anatomy of a plugin From 0c9c2c91f5f51d29d355622c0c5c314433c63662 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Jan 2021 16:30:18 -0700 Subject: [PATCH 60/60] [browserslist] remove unnecessary browsers (#89186) Co-authored-by: spalger --- .browserslistrc | 8 ++++---- .../basic_optimization.test.ts.snap | 2 +- yarn.lock | 18 ++++-------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.browserslistrc b/.browserslistrc index 36298c0f8cb93..c54816e60aebe 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -2,10 +2,10 @@ last 2 Firefox versions last 2 Chrome versions last 2 Safari versions -> 0.25% -not ie 11 -not op_mini all -not samsung 4 +last 2 Edge versions +last 1 ios_saf versions +last 1 and_chr versions +last 1 samsung versions [dev] last 1 chrome versions diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4681057ad0998..f23d8c9936980 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -108,4 +108,4 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i