From 9cebff12980d7afe2e4b13fcb15c26c627f38498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 5 Apr 2021 10:51:56 -0400 Subject: [PATCH 001/131] [OBS]home page is showing incorrect value of APM throughput (tpm) (#95991) * fixing obs transaction per minute value * addressing PR comments * fixing unit test * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...pm_observability_overview_fetchers.test.ts | 20 ++-- .../apm_observability_overview_fetchers.ts | 12 +-- .../get_transaction_coordinates.ts | 64 ------------- .../get_transactions_per_minute.ts | 95 +++++++++++++++++++ .../server/routes/observability_overview.ts | 8 +- .../components/app/section/apm/index.test.tsx | 26 +++++ .../components/app/section/apm/index.tsx | 15 ++- .../typings/fetch_overview_data/index.ts | 2 +- .../observability_overview.ts | 19 ++-- 9 files changed, 166 insertions(+), 95 deletions(-) delete mode 100644 x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts create mode 100644 x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 1821e92ee5a78..29fabc51fd582 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -46,11 +46,14 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 10, - transactionCoordinates: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ], + transactionPerMinute: { + value: 2, + timeseries: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -81,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [], + transactionPerMinute: { value: null, timeseries: [] }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -108,7 +111,10 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + transactionPerMinute: { + value: 0, + timeseries: [{ x: 1 }, { x: 2 }, { x: 3 }], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 55ead8d942aca..3a02efd05e5a5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { mean } from 'lodash'; import { ApmFetchDataResponse, FetchDataParams, @@ -31,7 +30,7 @@ export const fetchObservabilityOverviewPageData = async ({ }, }); - const { serviceCount, transactionCoordinates } = data; + const { serviceCount, transactionPerMinute } = data; return { appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, @@ -42,17 +41,12 @@ export const fetchObservabilityOverviewPageData = async ({ }, transactions: { type: 'number', - value: - mean( - transactionCoordinates - .map(({ y }) => y) - .filter((y) => y && isFinite(y)) - ) || 0, + value: transactionPerMinute.value || 0, }, }, series: { transactions: { - coordinates: transactionCoordinates, + coordinates: transactionPerMinute.timeseries, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts deleted file mode 100644 index aac18e2bdfe4c..0000000000000 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rangeQuery } from '../../../server/utils/queries'; -import { Coordinates } from '../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; - -export function getTransactionCoordinates({ - setup, - bucketSize, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange; - bucketSize: string; - searchAggregatedTransactions: boolean; -}): Promise { - return withApmSpan( - 'observability_overview_get_transaction_distribution', - async () => { - const { apmEventClient, start, end } = setup; - - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, - }, - aggs: { - distribution: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - }, - }, - }, - }); - - return ( - aggregations?.distribution.buckets.map((bucket) => ({ - x: bucket.key, - y: calculateThroughput({ start, end, value: bucket.doc_count }), - })) || [] - ); - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts new file mode 100644 index 0000000000000..da8ac7c50b594 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { rangeQuery } from '../../../server/utils/queries'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; +import { withApmSpan } from '../../utils/with_apm_span'; + +export function getTransactionsPerMinute({ + setup, + bucketSize, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; + searchAggregatedTransactions: boolean; +}) { + return withApmSpan( + 'observability_overview_get_transactions_per_minute', + async () => { + const { apmEventClient, start, end } = setup; + + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, + }, + }, + }, + }, + }, + }); + + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } + + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; + + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; + } + ); +} diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index b9c0a76b6fb90..1aac2c09d01c5 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; -import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; +import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; @@ -39,18 +39,18 @@ export const observabilityOverviewRoute = createRoute({ ); return withApmSpan('observability_overview', async () => { - const [serviceCount, transactionCoordinates] = await Promise.all([ + const [serviceCount, transactionPerMinute] = await Promise.all([ getServiceCount({ setup, searchAggregatedTransactions, }), - getTransactionCoordinates({ + getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, }), ]); - return { serviceCount, transactionCoordinates }; + return { serviceCount, transactionPerMinute }; }); }, }); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index e5f100be285e1..d29481a39eb72 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -56,6 +56,32 @@ describe('APMSection', () => { } as unknown) as ObservabilityPublicPluginsStart, })); }); + + it('renders transaction stat less then 1k', () => { + const resp = { + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 900, type: 'number' }, + }, + series: { + transactions: { coordinates: [] }, + }, + }; + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: resp, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const { getByText, queryAllByTestId } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Services 11')).toBeInTheDocument(); + expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 91a536840ecbd..e71468d3b028c 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -31,6 +31,19 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } +function formatTpmStat(value?: number) { + if (!value || value === 0) { + return '0'; + } + if (value <= 0.1) { + return '< 0.1'; + } + if (value > 1000) { + return numeral(value).format('0.00a'); + } + return numeral(value).format('0,0.0'); +} + export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); @@ -93,7 +106,7 @@ export function APMSection({ bucketSize }: Props) { ({ x: new Date(x).toISOString(), @@ -67,23 +68,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "x": "2020-12-08T13:57:00.000Z", - "y": 0.166666666666667, + "y": 2, }, Object { "x": "2020-12-08T13:58:00.000Z", - "y": 5.23333333333333, + "y": 61, }, Object { "x": "2020-12-08T13:59:00.000Z", - "y": 4.4, + "y": 36, }, Object { "x": "2020-12-08T14:00:00.000Z", - "y": 5.73333333333333, + "y": 75, }, Object { "x": "2020-12-08T14:01:00.000Z", - "y": 4.33333333333333, + "y": 36, }, ] `); From 123f3400a82f89a7be5a6c2d9526e62102530c47 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 5 Apr 2021 10:19:32 -0500 Subject: [PATCH 002/131] [Workplace Search] Add sub nav and fix rendering bugs in Personal dashboard (#96100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix route for private deferated source summary * Make schema types nullable Federated sources don’t have counts and the server returns null so our routes have to expect that sometimes these values will be null * Add SourceSubNav to Personal dashboard We are able to leverage the existing component with a couple a small change; the existing componet is a subnav in the larger Enterprise Search shared navigation component and does not include its styles. This caused the list items to render with bullet points next to them. Adding this class and displaying the nav items as block elements fixes this issue. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/components/source_sub_nav.tsx | 4 ++-- .../views/content_sources/private_sources_layout.test.tsx | 3 +++ .../views/content_sources/private_sources_layout.tsx | 3 +++ .../views/content_sources/source_logic.test.ts | 2 +- .../workplace_search/views/content_sources/source_logic.ts | 2 +- .../workplace_search/views/content_sources/sources.scss | 6 ++++++ .../server/routes/workplace_search/sources.ts | 6 +++--- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 99cebd5ded585..bf0c5471f7b57 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -33,7 +33,7 @@ export const SourceSubNav: React.FC = () => { const isCustom = serviceType === CUSTOM_SERVICE_TYPE; return ( - <> +
{NAV.OVERVIEW} @@ -53,6 +53,6 @@ export const SourceSubNav: React.FC = () => { {NAV.SETTINGS} - +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 488eb4b49853b..9e3b50ea083eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -17,6 +17,8 @@ import { EuiCallOut } from '@elastic/eui'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, @@ -40,6 +42,7 @@ describe('PrivateSourcesLayout', () => { const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index bdc2421432c8a..2a6281075dc40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -14,6 +14,8 @@ import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -49,6 +51,7 @@ export const PrivateSourcesLayout: React.FC = ({ + {readOnlyMode && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index d20d0576d11ce..a9712cc4e1dc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -214,7 +214,7 @@ describe('SourceLogic', () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); expect(http.get).toHaveBeenCalledWith( - '/api/workplace_search/org/sources/123/federated_summary' + '/api/workplace_search/account/sources/123/federated_summary' ); await promise; expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); 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 72700ce42c75d..3da90c4fc7739 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 @@ -156,7 +156,7 @@ export const SourceLogic = kea>({ } }, initializeFederatedSummary: async ({ sourceId }) => { - const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + const route = `/api/workplace_search/account/sources/${sourceId}/federated_summary`; try { const response = await HttpLogic.values.http.get(route); actions.onUpdateSummary(response.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index f142567fb621f..abab139e32369 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -30,3 +30,9 @@ margin-left: -$sideBarWidth; } } + +.sourcesSubNav { + li { + display: block; + } +} diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 8257dd0dc52b0..1dd6d859d88ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -22,9 +22,9 @@ const schemaValuesSchema = schema.recordOf( ); const pageSchema = schema.object({ - current: schema.number(), - size: schema.number(), - total_pages: schema.number(), + current: schema.nullable(schema.number()), + size: schema.nullable(schema.number()), + total_pages: schema.nullable(schema.number()), total_results: schema.number(), }); From ea03eb1bab086b6e6e526d8c1eb11ffa877d7d52 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 5 Apr 2021 10:27:05 -0500 Subject: [PATCH 003/131] [Enterprise Search] Expose core.chrome.setIsVisible for use in Workplace Search (#95984) * Hide chrome for Workplace Search by default The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes, which will be in a future commit * Add core.chrome.setIsVisible to KibanaLogic * Toggle chrome visibility for Workplace Search * Add test * Refactor to set context and chrome when pathname changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../applications/__mocks__/kibana_logic.mock.ts | 1 + .../enterprise_search/public/applications/index.tsx | 1 + .../applications/shared/kibana/kibana_logic.ts | 2 ++ .../applications/workplace_search/index.test.tsx | 4 +++- .../public/applications/workplace_search/index.tsx | 12 +++++++----- x-pack/plugins/enterprise_search/public/plugin.ts | 3 +++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 133f704fd59a9..2325ddcf2b270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -19,6 +19,7 @@ export const mockKibanaValues = { history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), + setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), renderHeaderActions: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 155ff5b92ba27..c2bf77751528a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ( history: params.history, navigateToUrl: core.application.navigateToUrl, setBreadcrumbs: core.chrome.setBreadcrumbs, + setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, renderHeaderActions: (HeaderActions) => params.setHeaderActionMenu((el) => renderHeaderActions(HeaderActions, store, el)), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 8015d22f7c44a..2bef7d373f160 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -24,6 +24,7 @@ interface KibanaLogicProps { charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } @@ -47,6 +48,7 @@ export const KibanaLogic = kea>({ {}, ], setBreadcrumbs: [props.setBreadcrumbs, {}], + setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], renderHeaderActions: [props.renderHeaderActions, {}], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 48bdcd6551b65..a2c0ec18def4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -57,11 +57,13 @@ describe('WorkplaceSearchConfigured', () => { setMockActions({ initializeAppData, setContext }); }); - it('renders layout and header actions', () => { + it('renders layout, chrome, and header actions', () => { const wrapper = shallow(); expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(OverviewMVP)).toHaveLength(1); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c269a987dc092..7a76de43be41b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,7 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); - const { renderHeaderActions } = useValues(KibanaLogic); + const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); const { pathname } = useLocation(); @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + useEffect(() => { + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - const isOrganization = !pathname.match(personalSourceUrlRegex); - setContext(isOrganization); + setContext(isOrganization); + setChromeIsVisible(isOrganization); + }, [pathname]); useEffect(() => { if (!hasInitialized) { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f00e81a5accf7..dd1a62d243d03 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -114,6 +114,9 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + // The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally + // here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes. + chrome.setIsVisible(false); await this.getInitialData(http); const pluginData = this.getPluginData(); From 95e45ddebc6e86bb3d63f04bd7c4e56ad8ef55bb Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 5 Apr 2021 18:00:13 +0200 Subject: [PATCH 004/131] Use plugin version in its publicPath (#95945) * Use plugin version in its publicPath * remove useless comment * fix types * update generated doc --- .../register_bundle_routes.test.ts | 15 +++++++------ .../bundle_routes/register_bundle_routes.ts | 8 +++---- src/core/server/legacy/legacy_service.test.ts | 1 + .../server/plugins/plugins_service.test.ts | 14 ++++++++++--- src/core/server/plugins/plugins_service.ts | 1 + src/core/server/plugins/plugins_system.ts | 1 + src/core/server/plugins/types.ts | 13 +++++++++--- .../bootstrap/get_plugin_bundle_paths.test.ts | 21 ++++++++++++------- .../bootstrap/get_plugin_bundle_paths.ts | 10 +++++++-- src/core/server/server.api.md | 8 +++---- 10 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts index d51c369146957..830f4a9a94364 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -10,7 +10,7 @@ import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks' import { PackageInfo } from '@kbn/config'; import { httpServiceMock } from '../../http/http_service.mock'; -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; @@ -29,9 +29,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ internal: ids.reduce((map, id) => { map.set(id, { publicTargetDir: `/plugins/${id}/public-target-dir`, + publicAssetsDir: `/plugins/${id}/public-assets-dir`, + version: '8.0.0', + requiredBundles: [], }); return map; - }, new Map()), + }, new Map()), }); describe('registerBundleRoutes', () => { @@ -86,16 +89,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', - routePath: '/42/bundles/plugin/plugin-a/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', + routePath: '/42/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', - routePath: '/42/bundles/plugin/plugin-b/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', + routePath: '/42/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index ee54f8ef34622..df46753747f5b 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -27,7 +27,7 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, // serverBasePath + serverBasePath, uiPlugins, packageInfo, }: { @@ -57,10 +57,10 @@ export function registerBundleRoutes({ isDist, }); - [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/`, + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d0a02b9859960..67b5393f0b838 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -91,6 +91,7 @@ beforeEach(() => { 'plugin-id', { requiredBundles: [], + version: '8.0.0', publicTargetDir: 'path/to/target/public', publicAssetsDir: '/plugins/name/assets/', }, diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 2d54648d22950..6bf7a1fadb4d3 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -562,12 +562,12 @@ describe('PluginsService', () => { plugin$: from([ createPlugin('plugin-1', { path: 'path-1', - version: 'some-version', + version: 'version-1', configPath: 'plugin1', }), createPlugin('plugin-2', { path: 'path-2', - version: 'some-version', + version: 'version-2', configPath: 'plugin2', }), ]), @@ -577,7 +577,7 @@ describe('PluginsService', () => { }); describe('uiPlugins.internal', () => { - it('includes disabled plugins', async () => { + it('contains internal properties for plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` @@ -586,15 +586,23 @@ describe('PluginsService', () => { "publicAssetsDir": /path-1/public/assets, "publicTargetDir": /path-1/target/public, "requiredBundles": Array [], + "version": "version-1", }, "plugin-2" => Object { "publicAssetsDir": /path-2/public/assets, "publicTargetDir": /path-2/target/public, "requiredBundles": Array [], + "version": "version-2", }, } `); }); + + it('includes disabled plugins', async () => { + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); + expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + }); }); describe('plugin initialization', () => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 8b33e2cf4cc6b..09be40ecaf2a2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -222,6 +222,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a6086bd6f17e8..3a01049c5e1fe 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -224,12 +224,15 @@ export interface DiscoveredPlugin { */ export interface InternalPluginInfo { /** - * Bundles that must be loaded for this plugoin + * Version of the plugin + */ + readonly version: string; + /** + * Bundles that must be loaded for this plugin */ readonly requiredBundles: readonly string[]; /** - * Path to the target/public directory of the plugin which should be - * served + * Path to the target/public directory of the plugin which should be served */ readonly publicTargetDir: string; /** @@ -250,7 +253,9 @@ export interface Plugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; } @@ -267,7 +272,9 @@ export interface AsyncPlugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; } diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index ea3843884df31..0abd8fd5a0057 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -16,12 +16,13 @@ const createUiPlugins = (pluginDeps: Record) => { browserConfigs: new Map(), }; - Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + const addPlugin = (pluginId: string, deps: string[]) => { uiPlugins.internal.set(pluginId, { requiredBundles: deps, + version: '8.0.0', publicTargetDir: '', publicAssetsDir: '', - } as any); + } as InternalPluginInfo); uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', @@ -29,6 +30,12 @@ const createUiPlugins = (pluginDeps: Record) => { requiredPlugins: [], requiredBundles: deps, }); + + deps.forEach((dep) => addPlugin(dep, [])); + }; + + Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + addPlugin(pluginId, deps); }); return uiPlugins; @@ -56,13 +63,13 @@ describe('getPluginsBundlePaths', () => { }); expect(pluginBundlePaths.get('a')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js', - publicPath: '/regular-bundle-path/plugin/a/', + bundlePath: '/regular-bundle-path/plugin/a/8.0.0/a.plugin.js', + publicPath: '/regular-bundle-path/plugin/a/8.0.0/', }); expect(pluginBundlePaths.get('b')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js', - publicPath: '/regular-bundle-path/plugin/b/', + bundlePath: '/regular-bundle-path/plugin/b/8.0.0/b.plugin.js', + publicPath: '/regular-bundle-path/plugin/b/8.0.0/', }); }); }); diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts index c8291b2720a92..86ffdcf835f7b 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts @@ -25,9 +25,15 @@ export const getPluginsBundlePaths = ({ while (pluginsToProcess.length > 0) { const pluginId = pluginsToProcess.pop() as string; + const plugin = uiPlugins.internal.get(pluginId); + if (!plugin) { + continue; + } + const { version } = plugin; + pluginBundlePaths.set(pluginId, { - publicPath: `${regularBundlePath}/plugin/${pluginId}/`, - bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`, + publicPath: `${regularBundlePath}/plugin/${pluginId}/${version}/`, + bundlePath: `${regularBundlePath}/plugin/${pluginId}/${version}/${pluginId}.plugin.js`, }); const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? []; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index de96c5ccfb81e..fb5fe3efd3e06 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -3259,9 +3259,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:296:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:401:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` From 8e11e2598e874d603e99bcfac407ef9e09784102 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 5 Apr 2021 12:04:20 -0400 Subject: [PATCH 005/131] [Maps] Enable all zoom levels for all users (#96093) --- .github/CODEOWNERS | 1 - docs/developer/plugin-list.asciidoc | 4 - packages/kbn-optimizer/limits.yml | 1 - .../service_settings/service_settings.test.js | 50 ++------- .../service_settings/service_settings.ts | 17 +-- .../service_settings_types.ts | 2 - src/plugins/maps_legacy/kibana.json | 2 +- .../public/map/base_maps_visualization.js | 3 +- .../maps_legacy/public/map/kibana_map.js | 23 ---- .../maps_legacy/public/map/map_messages.js | 105 ------------------ test/functional/apps/visualize/_tile_map.ts | 59 ---------- tsconfig.json | 1 - tsconfig.refs.json | 1 - .../plugins/maps_legacy_licensing/README.md | 4 - .../plugins/maps_legacy_licensing/kibana.json | 8 -- .../maps_legacy_licensing/public/index.ts | 12 -- .../maps_legacy_licensing/public/plugin.ts | 48 -------- .../maps_legacy_licensing/tsconfig.json | 15 --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 20 files changed, 12 insertions(+), 346 deletions(-) delete mode 100644 src/plugins/maps_legacy/public/map/map_messages.js delete mode 100644 x-pack/plugins/maps_legacy_licensing/README.md delete mode 100644 x-pack/plugins/maps_legacy_licensing/kibana.json delete mode 100644 x-pack/plugins/maps_legacy_licensing/public/index.ts delete mode 100644 x-pack/plugins/maps_legacy_licensing/public/plugin.ts delete mode 100644 x-pack/plugins/maps_legacy_licensing/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d14556ea1dabf..b9afc197bac9c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -146,7 +146,6 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis -#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index bcf74936077ec..691d7fb82f3bc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -452,10 +452,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] -|This plugin provides access to the detailed tile map services from Elastic. - - |{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] |This plugin provides access to the machine learning features provided by Elastic. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3c9fd4f59a406..a027768ad66a0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -51,7 +51,6 @@ pageLoadAssetSize: management: 46112 maps: 80000 mapsLegacy: 87859 - mapsLegacyLicensing: 20214 ml: 82187 monitoring: 80000 navigation: 37269 diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.test.js b/src/plugins/maps_ems/public/service_settings/service_settings.test.js index 5bd371aace79b..eb67997c253b9 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.test.js +++ b/src/plugins/maps_ems/public/service_settings/service_settings.test.js @@ -103,43 +103,8 @@ describe('service_settings (FKA tile_map test)', function () { expect(tmsService.attribution.includes('OpenStreetMap')).toEqual(true); }); - describe('modify - url', function () { - let tilemapServices; - + describe('tms mods', function () { let serviceSettings; - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach((key) => { - expect(urlObject.query[key]).toEqual(expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - serviceSettings = makeServiceSettings(); - // ensure that conflicts are overwritten - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - serviceSettings.setQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); it('should merge in tilemap url', async () => { serviceSettings = makeServiceSettings( @@ -161,7 +126,7 @@ describe('service_settings (FKA tile_map test)', function () { id: 'road_map', name: 'Road Map - Bright', url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl', minZoom: 0, maxZoom: 10, attribution: @@ -208,19 +173,19 @@ describe('service_settings (FKA tile_map test)', function () { ); expect(desaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationFalse.maxZoom).toEqual(10); expect(desaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationTrue.maxZoom).toEqual(18); expect(darkThemeDesaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationFalse.maxZoom).toEqual(22); expect(darkThemeDesaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationTrue.maxZoom).toEqual(22); }); @@ -264,14 +229,13 @@ describe('service_settings (FKA tile_map test)', function () { describe('File layers', function () { it('should load manifest (all props)', async function () { const serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach((key) => { + Object.keys({ elastic_tile_service_tos: 'agree' }).forEach((key) => { expect(typeof urlObject.query[key]).toEqual('string'); }); }); diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.ts b/src/plugins/maps_ems/public/service_settings/service_settings.ts index f7c735b6c3037..412db42a1570c 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings.ts @@ -22,7 +22,6 @@ export class ServiceSettings implements IServiceSettings { private readonly _mapConfig: MapsEmsConfig; private readonly _tilemapsConfig: TileMapConfig; private readonly _hasTmsConfigured: boolean; - private _showZoomMessage: boolean; private readonly _emsClient: EMSClient; private readonly tmsOptionsFromConfig: any; @@ -31,7 +30,6 @@ export class ServiceSettings implements IServiceSettings { this._tilemapsConfig = tilemapsConfig; this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; - this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: getKibanaVersion(), @@ -45,6 +43,9 @@ export class ServiceSettings implements IServiceSettings { return fetch(...args); }, }); + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this._emsClient.addQueryParams({ license: 'sspl' }); const markdownIt = new MarkdownIt({ html: false, @@ -58,18 +59,6 @@ export class ServiceSettings implements IServiceSettings { }); } - shouldShowZoomMessage({ origin }: { origin: string }): boolean { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - enableZoomMessage(): void { - this._showZoomMessage = true; - } - - disableZoomMessage(): void { - this._showZoomMessage = false; - } - __debugStubManifestCalls(manifestRetrieval: () => Promise): { removeStub: () => void } { const oldGetManifest = this._emsClient.getManifest; diff --git a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts index 80a9aae835844..6b04bd200eba8 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts @@ -46,8 +46,6 @@ export interface IServiceSettings { getFileLayers(): Promise; getUrlForRegionLayer(layer: FileLayer): Promise; setQueryParams(params: { [p: string]: string }): void; - enableZoomMessage(): void; - disableZoomMessage(): void; getAttributesForTMSLayer( tmsServiceConfig: TmsLayer, isDesaturated: boolean, diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 8e283288e34b2..f321274791a3b 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,5 @@ "ui": true, "server": true, "requiredPlugins": ["mapsEms"], - "requiredBundles": ["kibanaReact", "visDefaultEditor", "mapsEms"] + "requiredBundles": ["visDefaultEditor", "mapsEms"] } diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 9cd574c5246e8..a261bcf6edd80 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -193,13 +193,12 @@ export function BaseMapsVisualizationProvider() { isDesaturated, isDarkMode ); - const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index eea8315419289..62dbbda2588a5 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -7,13 +7,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../maps_ems/common'; -import { getToasts } from '../kibana_services'; import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { @@ -479,22 +477,6 @@ export class KibanaMap extends EventEmitter { this._updateLegend(); } - _addMaxZoomMessage = (layer) => { - const zoomWarningMsg = createZoomWarningMsg( - getToasts(), - this.getZoomLevel, - this.getMaxZoomLevel - ); - - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - }; - setLegendPosition(position) { if (this._legendPosition === position) { if (!this._leafletLegendControl) { @@ -572,11 +554,6 @@ export class KibanaMap extends EventEmitter { }); this._leafletBaseLayer = baseLayer; - if (settings.options.showZoomMessage) { - baseLayer.on('add', () => { - this._addMaxZoomMessage(baseLayer); - }); - } this._leafletBaseLayer.addTo(this._leafletMap); this._leafletBaseLayer.bringToBack(); if (settings.options.minZoom > this._leafletMap.getZoom()) { diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js deleted file mode 100644 index f60d819f0b390..0000000000000 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import { toMountPoint } from '../../../kibana_react/public'; - -export const createZoomWarningMsg = (function () { - let disableZoomMsg = false; - const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); - - class ZoomWarning extends React.Component { - constructor(props) { - super(props); - this.state = { - disabled: false, - }; - } - - render() { - return ( -
-

- - {`default distribution `} - - ), - ems: ( - - {`Elastic Maps Service`} - - ), - wms: ( - - {`Custom WMS Configuration`} - - ), - configSettings: ( - - {`Custom TMS Using Config Settings`} - - ), - }} - /> -

- - { - this.setState( - { - disabled: true, - }, - () => this.props.onChange(this.state.disabled) - ); - }} - data-test-subj="suppressZoomWarnings" - > - {`Don't show again`} - -
- ); - } - } - - const zoomToast = { - title: 'No additional zoom levels', - text: toMountPoint(), - 'data-test-subj': 'maxZoomWarning', - }; - - return (toastService, getZoomLevel, getMaxZoomLevel) => { - return () => { - const zoomLevel = getZoomLevel(); - const maxMapZoom = getMaxZoomLevel(); - if (!disableZoomMsg && zoomLevel === maxMapZoom) { - toastService.addDanger(zoomToast); - } - }; - }; -})(); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 668aec6ac5783..3af467affa1fb 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -15,7 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', @@ -221,63 +220,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('zoom warning behavior', function describeIndexTests() { - // Zoom warning is only applicable to OSS - this.tags(['skipCloud', 'skipFirefox']); - - const waitForLoading = false; - let zoomWarningEnabled; - let last = false; - const toastDefaultLife = 6000; - - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - - zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); - log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - - const zoomLevel = 9; - for (let i = 0; i < zoomLevel; i++) { - await PageObjects.tileMap.clickMapZoomIn(); - } - }); - - beforeEach(async function () { - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - }); - - afterEach(async function () { - if (!last) { - await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - } - }); - - it('should show warning at zoom 10', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should continue providing zoom warning if left alone', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should suppress zoom warning if suppress warnings button clicked', async () => { - last = true; - await PageObjects.visChart.waitForVisualization(); - await testSubjects.click('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - - await testSubjects.missingOrFail('maxZoomWarning'); - }); - }); }); } diff --git a/tsconfig.json b/tsconfig.json index 30944ac71fcc8..7c06e80858640 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -181,7 +181,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 2d9ddc1b9e568..f13455a14b4df 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -85,7 +85,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/x-pack/plugins/maps_legacy_licensing/README.md b/x-pack/plugins/maps_legacy_licensing/README.md deleted file mode 100644 index 7c2ce84d848d4..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Tile Map Plugin - -This plugin provides access to the detailed tile map services from Elastic. - diff --git a/x-pack/plugins/maps_legacy_licensing/kibana.json b/x-pack/plugins/maps_legacy_licensing/kibana.json deleted file mode 100644 index 7a49e0aaa7be1..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "mapsLegacyLicensing", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": false, - "ui": true, - "requiredPlugins": ["licensing", "mapsEms"] -} diff --git a/x-pack/plugins/maps_legacy_licensing/public/index.ts b/x-pack/plugins/maps_legacy_licensing/public/index.ts deleted file mode 100644 index 9105919eaa635..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/public/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MapsLegacyLicensing } from './plugin'; - -export function plugin() { - return new MapsLegacyLicensing(); -} diff --git a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts deleted file mode 100644 index f8118575cd6a2..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { LicensingPluginSetup, ILicense } from '../../licensing/public'; -import { IServiceSettings, MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public'; - -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ - -export interface MapsLegacyLicensingSetupDependencies { - licensing: LicensingPluginSetup; - mapsEms: MapsEmsPluginSetup; -} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MapsLegacyLicensingStartDependencies {} - -export type MapsLegacyLicensingSetup = ReturnType; -export type MapsLegacyLicensingStart = ReturnType; - -export class MapsLegacyLicensing - implements Plugin { - public setup(core: CoreSetup, plugins: MapsLegacyLicensingSetupDependencies) { - const { licensing, mapsEms } = plugins; - if (licensing) { - licensing.license$.subscribe(async (license: ILicense) => { - const serviceSettings: IServiceSettings = await mapsEms.getServiceSettings(); - const { uid, isActive } = license; - if (isActive && license.hasAtLeast('basic')) { - serviceSettings.setQueryParams({ license: uid || '' }); - serviceSettings.disableZoomMessage(); - } else { - serviceSettings.setQueryParams({ license: '' }); - serviceSettings.enableZoomMessage(); - } - }); - } - } - - public start(core: CoreStart, plugins: MapsLegacyLicensingStartDependencies) {} -} diff --git a/x-pack/plugins/maps_legacy_licensing/tsconfig.json b/x-pack/plugins/maps_legacy_licensing/tsconfig.json deleted file mode 100644 index 3b8102b5205a8..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.project.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*"], - "references": [ - { "path": "../licensing/tsconfig.json" }, - { "path": "../../../src/plugins/maps_ems/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 133b4d0b6aaa8..6dc490b4ffc53 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3080,7 +3080,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子はdata-updateに対応できるようこのメソッドを導入する必要があります", "maps_legacy.defaultDistributionMessage": "Mapsを入手するには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", - "maps_legacy.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。{ems}ではより多くのズームレベルを無料で利用できます。または、独自のマップサーバーを構成できます。詳細は、{ wms }または{ configSettings}をご覧ください。", "maps_legacy.legacyMapDeprecationMessage": "Mapsを使用すると、複数のレイヤーとインデックスを追加する、個別のドキュメントをプロットする、データ値から特徴を表現する、ヒートマップ、グリッド、クラスターを追加するなど、さまざまなことが可能です。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "{label}は8.0でMapsに移行されます。", "maps_legacy.openInMapsButtonLabel": "Mapsで表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0f9d8b90a2578..32574690b13f2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3101,7 +3101,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子对象应实现此方法以响应数据更新", "maps_legacy.defaultDistributionMessage": "要获取 Maps,请升级到 {defaultDistribution} 版的 Elasticsearch 和 Kibana。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", - "maps_legacy.kibanaMap.zoomWarning": "已达到缩放级别数目上限。要一直放大,请升级到 Elasticsearch 和 Kibana 的{defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "maps_legacy.legacyMapDeprecationMessage": "使用 Maps,可以添加多个图层和索引,绘制单个文档,使用数据值表示特征,添加热图、网格和集群,等等。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "在 8.0 中,{label} 将迁移到 Maps。", "maps_legacy.openInMapsButtonLabel": "在 Maps 中查看", From bcb72c596a438f6a223e875395380afe1efc291c Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 5 Apr 2021 11:39:09 -0600 Subject: [PATCH 006/131] [RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to implement `renderCellValue` (#96098) ### [RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to implement `renderCellValue` - This PR implements a superset of the `renderCellValue` API from [EuiDataGrid](https://elastic.github.io/eui/#/tabular-content/data-grid) in the `TGrid` (Timeline grid) API - The TGrid API was also updated to accept a collection of `RowRenderer`s as a prop The API changes are summarized by the following screenshot: render-cell-value The following screenshot shows the `signal.rule.risk_score` column in the Alerts table being rendered with a green background color, using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs): alerts Note: In the screenshot above, the values in the Alerts table are also _not_ rendered as draggables. Related (RAC) issue: https://github.com/elastic/kibana/issues/94520 ### Details The `StatefulEventsViewer` has been updated to accept `renderCellValue` as a (required) prop: ``` renderCellValue: (props: CellValueElementProps) => React.ReactNode; ``` The type definition of `CellValueElementProps` is: ``` export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; eventId: string; // _id header: ColumnHeaderOptions; linkValues: string[] | undefined; timelineId: string; }; ``` The `CellValueElementProps` type above is a _superset_ of `EuiDataGridCellValueElementProps`. The additional properties above include the `data` returned by the TGrid when it performs IO to retrieve alerts and events. ### Using `renderCellValue` to control rendering The internal implementation of TGrid's cell rendering didn't change with this PR; it moved to `x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` as shown below: ``` export const DefaultCellRenderer: React.FC = ({ columnId, data, eventId, header, linkValues, setCellProps, timelineId, }) => ( <> {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ columnName: header.id, eventId, field: header, linkValues, timelineId, truncate: true, values: getMappedNonEcsValue({ data, fieldName: header.id, }), })} ); ``` Any usages of TGrid were updated to pass `DefaultCellRenderer` as the value of the `renderCellValue` prop, as shown in the screenshot below: render-cell-value The `EuiDataGrid` [codesandbox example](https://codesandbox.io/s/nsmzs) provides the following example `renderCellValue` implementation, which highlights a cell green based on it's numeric value: ``` const renderCellValue = useMemo(() => { return ({ rowIndex, columnId, setCellProps }) => { const data = useContext(DataContext); useEffect(() => { if (columnId === 'amount') { if (data.hasOwnProperty(rowIndex)) { const numeric = parseFloat( data[rowIndex][columnId].match(/\d+\.\d+/)[0], 10 ); setCellProps({ style: { backgroundColor: `rgba(0, 255, 0, ${numeric * 0.0002})`, }, }); } } }, [rowIndex, columnId, setCellProps, data]); function getFormatted() { return data[rowIndex][columnId].formatted ? data[rowIndex][columnId].formatted : data[rowIndex][columnId]; } return data.hasOwnProperty(rowIndex) ? getFormatted(rowIndex, columnId) : null; }; }, []); ``` The sample code above formats the `amount` column in the example `EuiDataGrid` with a green `backgroundColor` based on the value of the data, as shown in the screenshot below: datagrid-cell-formatting To demonstrate that similar styling can be applied to TGrid using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs), we can update the `DefaultCellRenderer` in `x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` to apply a similar technique: ``` export const DefaultCellRenderer: React.FC = ({ columnId, data, eventId, header, linkValues, setCellProps, timelineId, }) => { useEffect(() => { if (columnId === 'signal.rule.risk_score') { const value = getMappedNonEcsValue({ data, fieldName: columnId, }); if (Array.isArray(value) && value.length > 0) { const numeric = parseFloat(value[0]); setCellProps({ style: { backgroundColor: `rgba(0, 255, 0, ${numeric * 0.002})`, }, }); } } }, [columnId, data, setCellProps]); return ( <> {getMappedNonEcsValue({ data, fieldName: columnId, })} ); }; ``` The example code above renders the `signal.rule.risk_score` column in the Alerts table with a green `backgroundColor` based on the value of the data, as shown in the screenshot below: alerts Note: In the screenshot above, the values in the Alerts table are not rendered as draggables. --- .../components/alerts_viewer/alerts_table.tsx | 4 + .../events_viewer/events_viewer.test.tsx | 6 + .../events_viewer/events_viewer.tsx | 14 +- .../components/events_viewer/index.test.tsx | 4 + .../common/components/events_viewer/index.tsx | 13 +- .../components/alerts_table/index.tsx | 4 + .../navigation/events_query_tab_body.tsx | 4 + .../components/flyout/pane/index.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 553 ++++++++-- .../body/data_driven_columns/index.test.tsx | 4 +- .../body/data_driven_columns/index.tsx | 30 +- .../stateful_cell.test.tsx | 171 +++ .../data_driven_columns/stateful_cell.tsx | 63 ++ .../body/events/event_column_view.test.tsx | 2 + .../body/events/event_column_view.tsx | 8 +- .../components/timeline/body/events/index.tsx | 8 +- .../timeline/body/events/stateful_event.tsx | 10 +- .../components/timeline/body/index.test.tsx | 8 +- .../components/timeline/body/index.tsx | 13 +- .../body/renderers/get_row_renderer.test.tsx | 16 +- .../timeline/body/renderers/index.ts | 2 +- .../default_cell_renderer.test.tsx | 107 ++ .../cell_rendering/default_cell_renderer.tsx | 39 + .../timeline/cell_rendering/index.tsx | 20 + .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../timeline/eql_tab_content/index.test.tsx | 4 + .../timeline/eql_tab_content/index.tsx | 8 + .../components/timeline/index.test.tsx | 4 + .../timelines/components/timeline/index.tsx | 14 +- .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../pinned_tab_content/index.test.tsx | 5 +- .../timeline/pinned_tab_content/index.tsx | 8 + .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../timeline/query_tab_content/index.test.tsx | 4 + .../timeline/query_tab_content/index.tsx | 8 + .../timeline/tabs_content/index.tsx | 64 +- .../timeline/epic_local_storage.test.tsx | 5 +- 37 files changed, 4047 insertions(+), 128 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index af90d17fe62b8..43d5c66655808 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -12,6 +12,8 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; import { SourcererScopeName } from '../../store/sourcerer/model'; @@ -91,6 +93,8 @@ const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={endDate} id={timelineId} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 3ecc17589fe08..8962f5e6c5146 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -26,6 +26,8 @@ import { KqlMode } from '../../../timelines/store/timeline/model'; import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/components/graph_overlay', () => ({ @@ -99,6 +101,8 @@ const eventsViewerDefaultProps = { query: '', language: 'kql', }, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, sort: [ { @@ -118,6 +122,8 @@ describe('EventsViewer', () => { defaultModel: eventsDefaultModel, end: to, id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, scopeId: SourcererScopeName.timeline, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 050cd92b0556e..e6e868f1a7365 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; @@ -41,7 +40,9 @@ import { useManageTimeline } from '../../../timelines/components/manage_timeline import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -122,6 +123,8 @@ interface Props { kqlMode: KqlMode; query: Query; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; start: string; sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -146,8 +149,10 @@ const EventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, sort, utilityBar, @@ -310,6 +315,8 @@ const EventsViewerComponent: React.FC = ({ isEventViewer={true} onRuleChange={onRuleChange} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ @@ -343,6 +350,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && @@ -359,6 +367,8 @@ export const EventsViewer = React.memo( prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 5004c23f9111c..cd27177643b44 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -18,7 +18,9 @@ import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), @@ -38,6 +40,8 @@ const testProps = { end: to, indexNames: [], id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, scopeId: SourcererScopeName.default, start: from, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 59dc756bb2b3e..b58aa2236d292 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -22,6 +22,8 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { DetailsPanel } from '../../../timelines/components/side_panel'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -41,6 +43,8 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; } @@ -67,8 +71,10 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPageOptions, kqlMode, pageFilters, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, scopeId, showCheckboxes, @@ -129,6 +135,8 @@ const StatefulEventsViewerComponent: React.FC = ({ kqlMode={kqlMode} query={query} onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} start={start} sort={sort} utilityBar={utilityBar} @@ -201,6 +209,7 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && prevProps.scopeId === nextProps.scopeId && @@ -215,6 +224,8 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 6c88b8e29800b..cf6db52d0cece 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -48,6 +48,8 @@ import { import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { buildTimeRangeFilter } from './helpers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -336,6 +338,8 @@ export const AlertsTableComponent: React.FC = ({ headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 922d52b6cfe5a..f88709e6e95ac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -21,6 +21,8 @@ import { useGlobalFullScreen } from '../../../common/containers/use_full_screen' import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; @@ -96,6 +98,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ defaultModel={eventsDefaultModel} end={endDate} id={TimelineId.hostsPageEvents} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} pageFilters={pageFilters} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index e63ffedf3da7c..459706de36569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -14,6 +14,8 @@ import { StatefulTimeline } from '../../timeline'; import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; +import { defaultRowRenderers } from '../../timeline/body/renderers'; +import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { @@ -46,7 +48,11 @@ const FlyoutPaneComponent: React.FC = ({ timelineId }) onClose={handleClose} size="l" > - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 72d2956bd4086..91d039a19495c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -22,26 +22,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 2

- @@ -63,15 +114,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 3

- @@ -93,15 +206,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 4

- @@ -123,15 +298,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 5

- @@ -153,15 +390,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 6

- @@ -183,15 +482,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 7

- @@ -213,15 +574,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 8

- diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index f20978c6ba726..234e28e6231c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -9,10 +9,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; -import { columnRenderers } from '../renderers'; import { DataDrivenColumns } from '.'; @@ -25,11 +25,11 @@ describe('Columns', () => { ariaRowindex={2} _id={mockTimelineData[0]._id} columnHeaders={headersSansTimestamp} - columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} hasRowRenderers={false} notesCount={0} + renderCellValue={DefaultCellRenderer} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 5aba562749f01..aeb9af46ea2ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -9,6 +9,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import { getOr } from 'lodash/fp'; +import { CellValueElementProps } from '../../cell_rendering'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -16,20 +17,19 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { getColumnRenderer } from '../renderers/get_column_renderer'; +import { StatefulCell } from './stateful_cell'; import * as i18n from './translations'; interface Props { _id: string; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; hasRowRenderers: boolean; notesCount: number; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; tabType?: TimelineTabs; timelineId: string; } @@ -82,11 +82,11 @@ export const DataDrivenColumns = React.memo( _id, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, hasRowRenderers, notesCount, + renderCellValue, tabType, timelineId, }) => ( @@ -105,18 +105,16 @@ export const DataDrivenColumns = React.memo(

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: i + 2 })}

- {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} + {hasRowRenderers ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx new file mode 100644 index 0000000000000..3c75bc7fb2649 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React, { useEffect } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../store/timeline/model'; + +import { StatefulCell } from './stateful_cell'; +import { getMappedNonEcsValue } from '.'; + +/** + * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, + * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid + * + * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. + * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, + * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: + * https://codesandbox.io/s/zhxmo + */ +const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { + useEffect(() => { + // branching logic that conditionally renders a specific cell green: + if (columnId === defaultHeaders[0].id) { + const value = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + + if (value?.length) { + setCellProps({ + style: { + backgroundColor: 'green', + }, + }); + } + } + }, [columnId, data, setCellProps]); + + return ( +
+ {getMappedNonEcsValue({ + data, + fieldName: columnId, + })} +
+ ); +}; + +describe('StatefulCell', () => { + const ariaRowindex = 123; + const eventId = '_id-123'; + const linkValues = ['foo', 'bar', '@baz']; + const tabType = TimelineTabs.query; + const timelineId = 'test'; + + let header: ColumnHeaderOptions; + let data: TimelineNonEcsData[]; + beforeEach(() => { + data = cloneDeep(mockTimelineData[0].data); + header = cloneDeep(defaultHeaders[0]); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId: `${timelineId}-${tabType}`, + }) + ); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId, + }) + ); + }); + + test('it renders the React.Node returned by renderCellValue', () => { + const renderCellValue = () =>
; + + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); + }); + + test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') + ).toEqual('background-color: green;'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx new file mode 100644 index 0000000000000..83f603364ba8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { HTMLAttributes, useState } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; + +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +const StatefulCellComponent = ({ + ariaRowindex, + data, + header, + eventId, + linkValues, + renderCellValue, + tabType, + timelineId, +}: { + ariaRowindex: number; + data: TimelineNonEcsData[]; + header: ColumnHeaderOptions; + eventId: string; + linkValues: string[] | undefined; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +}) => { + const [cellProps, setCellProps] = useState>({}); + + return ( +
+ {renderCellValue({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + setCellProps, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, + })} +
+ ); +}; + +StatefulCellComponent.displayName = 'StatefulCellComponent'; + +export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index abdfda3272d6a..74724dedf4d11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -14,6 +14,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; @@ -56,6 +57,7 @@ describe('EventColumnView', () => { onRowSelected: jest.fn(), onUnPinEvent: jest.fn(), refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, selectedEventIds: {}, showCheckboxes: false, showNotes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6caf0a7b5b15..a0a0aeb23e8f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo } from 'react'; +import { CellValueElementProps } from '../../cell_rendering'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -21,7 +22,6 @@ import { getPinOnClick, InvestigateInResolverAction, } from '../helpers'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { AddEventNoteAction } from '../actions/add_note_icon_item'; @@ -38,7 +38,6 @@ interface Props { actionsColumnWidth: number; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; @@ -51,6 +50,7 @@ interface Props { onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; hasRowRenderers: boolean; selectedEventIds: Readonly>; @@ -69,7 +69,6 @@ export const EventColumnView = React.memo( actionsColumnWidth, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, eventIdToNoteIds, @@ -84,6 +83,7 @@ export const EventColumnView = React.memo( refetch, hasRowRenderers, onRuleChange, + renderCellValue, selectedEventIds, showCheckboxes, showNotes, @@ -227,11 +227,11 @@ export const EventColumnView = React.memo( _id={id} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} ecsData={ecsData} hasRowRenderers={hasRowRenderers} notesCount={notesCount} + renderCellValue={renderCellValue} tabType={tabType} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index d76b5834c233e..7f8a3a92fb5ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; +import { CellValueElementProps } from '../../cell_rendering'; import { inputsModel } from '../../../../../common/store'; import { BrowserFields } from '../../../../../common/containers/source'; import { @@ -18,7 +19,6 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; import { eventIsPinned } from '../helpers'; @@ -30,7 +30,6 @@ interface Props { actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; containerRef: React.MutableRefObject; data: TimelineItem[]; eventIdToNoteIds: Readonly>; @@ -41,6 +40,7 @@ interface Props { onRowSelected: OnRowSelected; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; @@ -52,7 +52,6 @@ const EventsComponent: React.FC = ({ actionsColumnWidth, browserFields, columnHeaders, - columnRenderers, containerRef, data, eventIdToNoteIds, @@ -64,6 +63,7 @@ const EventsComponent: React.FC = ({ pinnedEventIds, refetch, onRuleChange, + renderCellValue, rowRenderers, selectedEventIds, showCheckboxes, @@ -76,7 +76,6 @@ const EventsComponent: React.FC = ({ ariaRowindex={i + ARIA_ROW_INDEX_OFFSET} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} containerRef={containerRef} event={event} eventIdToNoteIds={eventIdToNoteIds} @@ -88,6 +87,7 @@ const EventsComponent: React.FC = ({ lastFocusedAriaColindex={lastFocusedAriaColindex} loadingEventIds={loadingEventIds} onRowSelected={onRowSelected} + renderCellValue={renderCellValue} refetch={refetch} rowRenderers={rowRenderers} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4191badd6b03f..97ab088b61583 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { CellValueElementProps } from '../../cell_rendering'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineExpandedDetailType, @@ -23,7 +24,6 @@ import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/mod import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; @@ -45,7 +45,6 @@ interface Props { containerRef: React.MutableRefObject; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; @@ -56,6 +55,7 @@ interface Props { refetch: inputsModel.Refetch; ariaRowindex: number; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -77,7 +77,6 @@ const StatefulEventComponent: React.FC = ({ browserFields, containerRef, columnHeaders, - columnRenderers, event, eventIdToNoteIds, isEventViewer = false, @@ -86,8 +85,9 @@ const StatefulEventComponent: React.FC = ({ loadingEventIds, onRowSelected, refetch, - onRuleChange, + renderCellValue, rowRenderers, + onRuleChange, ariaRowindex, selectedEventIds, showCheckboxes, @@ -259,7 +259,6 @@ const StatefulEventComponent: React.FC = ({ actionsColumnWidth={actionsColumnWidth} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={event.data} ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} @@ -273,6 +272,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} refetch={refetch} + renderCellValue={renderCellValue} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 723e4c3de5c27..76dbfc553d228 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import '../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../../common/search_strategy'; @@ -19,6 +20,7 @@ import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; +import { defaultRowRenderers } from './renderers'; const mockSort: Sort[] = [ { @@ -39,8 +41,8 @@ jest.mock('react-redux', () => { }); jest.mock('../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), - useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useShallowEqualSelector: () => mockTimelineModel, + useDeepEqualSelector: () => mockTimelineModel, })); jest.mock('../../../../common/components/link_to'); @@ -76,6 +78,8 @@ describe('Body', () => { loadingEventIds: [], pinnedEventIds: {}, refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, selectedEventIds: {}, setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 4df6eb16ccb62..59c0610c544e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { CellValueElementProps } from '../cell_rendering'; import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, @@ -28,9 +29,9 @@ import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; -import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { RowRenderer } from './renderers/row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { Events } from './events'; @@ -44,6 +45,8 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort[]; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; tabType: TimelineTabs; totalPages: number; onRuleChange?: () => void; @@ -83,6 +86,8 @@ export const BodyComponent = React.memo( onRuleChange, showCheckboxes, refetch, + renderCellValue, + rowRenderers, sort, tabType, totalPages, @@ -141,7 +146,7 @@ export const BodyComponent = React.memo( if (!excludedRowRendererIds) return rowRenderers; return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds]); + }, [excludedRowRendererIds, rowRenderers]); const actionsColumnWidth = useMemo( () => @@ -209,7 +214,6 @@ export const BodyComponent = React.memo( actionsColumnWidth={actionsColumnWidth} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} eventIdToNoteIds={eventIdToNoteIds} id={id} @@ -219,6 +223,7 @@ export const BodyComponent = React.memo( onRowSelected={onRowSelected} pinnedEventIds={pinnedEventIds} refetch={refetch} + renderCellValue={renderCellValue} rowRenderers={enabledRowRenderers} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} @@ -244,6 +249,8 @@ export const BodyComponent = React.memo( prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.tabType === nextProps.tabType ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 6e36102da2de9..b92a4381d837b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -17,7 +17,7 @@ import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; -import { rowRenderers } from '.'; +import { defaultRowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; jest.mock('@elastic/eui', () => { @@ -48,7 +48,7 @@ describe('get_column_renderer', () => { }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -60,7 +60,7 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -75,7 +75,7 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -93,7 +93,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data if event.category is network_traffic', () => { suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -111,7 +111,7 @@ describe('get_column_renderer', () => { test('should render a zeek row data if event.category is network_traffic', () => { zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(zeek, rowRenderers); + const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, @@ -129,7 +129,7 @@ describe('get_column_renderer', () => { test('should render a system row data if event.category is network_traffic', () => { system.event = { ...system.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(system, rowRenderers); + const rowRenderer = getRowRenderer(system, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, @@ -147,7 +147,7 @@ describe('get_column_renderer', () => { test('should render a auditd row data if event.category is network_traffic', () => { auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(auditd, rowRenderers); + const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 671d183c62e6d..209a9414f62f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -23,7 +23,7 @@ import { systemRowRenderers } from './system/generic_row_renderer'; // Suricata and Zeek which is why Suricata and Zeek are above it. The // plainRowRenderer always returns true to everything which is why it always // should be last. -export const rowRenderers: RowRenderer[] = [ +export const defaultRowRenderers: RowRenderer[] = [ ...auditdRowRenderers, ...systemRowRenderers, suricataRowRenderer, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx new file mode 100644 index 0000000000000..5ac1dcf8805cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; +import { DefaultCellRenderer } from './default_cell_renderer'; + +jest.mock('../body/renderers/get_column_renderer'); +const getColumnRendererMock = getColumnRenderer as jest.Mock; +const mockImplementation = { + renderColumn: jest.fn(), +}; + +describe('DefaultCellRenderer', () => { + const columnId = 'signal.rule.risk_score'; + const eventId = '_id-123'; + const isDetails = true; + const isExpandable = true; + const isExpanded = true; + const linkValues = ['foo', 'bar', '@baz']; + const rowIndex = 3; + const setCellProps = jest.fn(); + const timelineId = 'test'; + + beforeEach(() => { + jest.clearAllMocks(); + getColumnRendererMock.mockImplementation(() => mockImplementation); + }); + + test('it invokes `getColumnRenderer` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); + }); + + test('it invokes `renderColumn` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(mockImplementation.renderColumn).toBeCalledWith({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: ['2018-11-05T19:03:25.937Z'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx new file mode 100644 index 0000000000000..8d8f821107e7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { getMappedNonEcsValue } from '../body/data_driven_columns'; +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; + +import { CellValueElementProps } from '.'; + +export const DefaultCellRenderer: React.FC = ({ + columnId, + data, + eventId, + header, + linkValues, + setCellProps, + timelineId, +}) => ( + <> + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx new file mode 100644 index 0000000000000..03e444e3a9afd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; + +import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; +import { ColumnHeaderOptions } from '../../../store/timeline/model'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + data: TimelineNonEcsData[]; + eventId: string; // _id + header: ColumnHeaderOptions; + linkValues: string[] | undefined; + timelineId: string; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap index 2595f29144b80..7d237ecaf92df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap @@ -140,6 +140,986 @@ In other use cases the message field can be used to concatenate different values ] } onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } showExpandedDetails={false} start="2018-03-23T18:49:23.132Z" timelineId="test" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 7b77a915f2f05..e13bed1e2eff6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -9,6 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import { defaultRowRenderers } from '../body/renderers'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -94,6 +96,8 @@ describe('Timeline', () => { itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showExpandedDetails: false, start: startDate, timerangeKind: 'absolute', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 51f8db4e796e5..6bb19ce5a6852 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -22,10 +22,12 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { calculateTotalPages } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; @@ -133,6 +135,8 @@ const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.timerangeKind === nextProps.timerangeKind; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -154,6 +158,8 @@ export const EqlTabContentComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, start, timerangeKind, @@ -284,6 +290,8 @@ export const EqlTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={NO_SORTING} tabType={TimelineTabs.eql} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ee2ce8cf8103b..db7a3cc3c9900 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -17,7 +17,9 @@ import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { DefaultCellRenderer } from './cell_rendering/default_cell_renderer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; +import { defaultRowRenderers } from './body/renderers'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -63,6 +65,8 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, timelineId: TimelineId.test, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 6d2374dd8eef7..367357511c9c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -14,6 +14,8 @@ import styled from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; +import { RowRenderer } from './body/renderers/row_renderer'; +import { CellValueElementProps } from './cell_rendering'; import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -36,10 +38,12 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: TimelineId; } -const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { +const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const isSaving = useShallowEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isSaving @@ -50,7 +54,11 @@ const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { const TimelineSavingProgress = React.memo(TimelineSavingProgressComponent); -const StatefulTimelineComponent: React.FC = ({ timelineId }) => { +const StatefulTimelineComponent: React.FC = ({ + renderCellValue, + rowRenderers, + timelineId, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -131,6 +139,8 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { { timelineId: TimelineId.test, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, sort, pinnedEventIds: {}, showExpandedDetails: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index a19a61d8268ff..dfc14747dacf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -14,10 +14,12 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -87,6 +89,8 @@ const VerticalRule = styled.div` VerticalRule.displayName = 'VerticalRule'; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -106,6 +110,8 @@ export const PinnedTabContentComponent: React.FC = ({ itemsPerPageOptions, pinnedEventIds, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, sort, }) => { @@ -217,6 +223,8 @@ export const PinnedTabContentComponent: React.FC = ({ data={events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.pinned} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index 0688a10b31eef..46c85f634ff6b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,6 +276,986 @@ In other use cases the message field can be used to concatenate different values kqlMode="search" kqlQueryExpression="" onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } show={true} showCallOutUnauthorizedMsg={false} showExpandedDetails={false} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index c7d27da64c650..ede473acbfb2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -10,11 +10,13 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { Direction } from '../../../../graphql/types'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from './index'; +import { defaultRowRenderers } from '../body/renderers'; import { Sort } from '../body/sort'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; @@ -106,6 +108,8 @@ describe('Timeline', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, sort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 28fec7ded9ca2..74a0f02354219 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -22,6 +22,8 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { useKibana } from '../../../../common/lib/kibana'; @@ -142,6 +144,8 @@ const compareQueryProps = (prevProps: Props, nextProps: Props) => deepEqual(prevProps.filters, nextProps.filters); interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -164,6 +168,8 @@ export const QueryTabContentComponent: React.FC = ({ kqlMode, kqlQueryExpression, onEventClosed, + renderCellValue, + rowRenderers, show, showCallOutUnauthorizedMsg, showExpandedDetails, @@ -330,6 +336,8 @@ export const QueryTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index f29211d519841..76a2ad0960322 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -20,6 +20,8 @@ import { TimelineEventsCountBadge, } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { getActiveTabSelector, getNoteIdsSelector, @@ -46,6 +48,8 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; setTimelineFullScreen?: (fullScreen: boolean) => void; timelineFullScreen?: boolean; timelineId: TimelineId; @@ -53,16 +57,32 @@ interface BasicTimelineTab { graphEventId?: string; } -const QueryTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const QueryTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); QueryTab.displayName = 'QueryTab'; -const EqlTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const EqlTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); EqlTab.displayName = 'EqlTab'; @@ -81,9 +101,17 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => )); NotesTab.displayName = 'NotesTab'; -const PinnedTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const PinnedTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); PinnedTab.displayName = 'PinnedTab'; @@ -91,7 +119,7 @@ PinnedTab.displayName = 'PinnedTab'; type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; const ActiveTimelineTab = memo( - ({ activeTimelineTab, timelineId, timelineType }) => { + ({ activeTimelineTab, renderCellValue, rowRenderers, timelineId, timelineType }) => { const getTab = useCallback( (tab: TimelineTabs) => { switch (tab) { @@ -119,14 +147,26 @@ const ActiveTimelineTab = memo( return ( <> - + - + {timelineType === TimelineType.default && ( - + )} @@ -160,6 +200,8 @@ const StyledEuiTab = styled(EuiTab)` `; const TabsContentComponent: React.FC = ({ + renderCellValue, + rowRenderers, timelineId, timelineFullScreen, timelineType, @@ -300,6 +342,8 @@ const TabsContentComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 3d92397f4ab50..0b70ba8991686 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -30,11 +30,12 @@ import { updateItemsPerPage, updateSort, } from './actions'; - +import { DefaultCellRenderer } from '../../components/timeline/cell_rendering/default_cell_renderer'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps, } from '../../components/timeline/query_tab_content'; +import { defaultRowRenderers } from '../../components/timeline/body/renderers'; import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction } from '../../../graphql/types'; @@ -90,6 +91,8 @@ describe('epicLocalStorage', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, start: startDate, From 1fad3175f9526882f4eab02829d73b75194e6b4e Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 5 Apr 2021 13:42:46 -0400 Subject: [PATCH 007/131] [Maps] Safe-erase text-field (#94873) --- .../properties/dynamic_text_property.test.tsx | 109 ++++++++++++++++++ .../properties/dynamic_text_property.ts | 4 +- .../properties/static_text_property.test.ts | 70 +++++++++++ .../vector/properties/static_text_property.ts | 4 +- 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx new file mode 100644 index 0000000000000..4550a27ac2d9a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('../components/vector_style_editor', () => ({ + VectorStyleEditor: () => { + return
mockVectorStyleEditor
; + }, +})); + +import React from 'react'; + +// @ts-ignore +import { DynamicTextProperty } from './dynamic_text_property'; +import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; +import { IField } from '../../../fields/field'; +import { Map as MbMap } from 'mapbox-gl'; +import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; +import { IVectorLayer } from '../../../layers/vector_layer'; + +export class MockMbMap { + _paintPropertyCalls: unknown[]; + _lastTextFieldValue: unknown | undefined; + + constructor(lastTextFieldValue?: unknown) { + this._paintPropertyCalls = []; + this._lastTextFieldValue = lastTextFieldValue; + } + setLayoutProperty(layerId: string, propName: string, value: undefined | 'string') { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + this._lastTextFieldValue = value; + this._paintPropertyCalls.push([layerId, value]); + } + + getLayoutProperty(layername: string, propName: string): unknown | undefined { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + return this._lastTextFieldValue; + } + + getPaintPropertyCalls(): unknown[] { + return this._paintPropertyCalls; + } +} + +const makeProperty = (mockStyle: MockStyle, field: IField | null) => { + return new DynamicTextProperty( + {}, + VECTOR_STYLES.LABEL_TEXT, + field, + (new MockLayer(mockStyle) as unknown) as IVectorLayer, + () => { + return (value: RawValue) => value + '_format'; + } + ); +}; + +describe('syncTextFieldWithMb', () => { + describe('with field', () => { + test('Should set', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), mockField); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([ + ['foobar', ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], '']], + ]); + }); + }); + + describe('without field', () => { + test('Should clear', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap([ + 'foobar', + ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], ''], + ]) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', undefined]]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts index 22ea3067b1748..e8612388a5ae1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts @@ -20,7 +20,9 @@ export class DynamicTextProperty extends DynamicStyleProperty { + return new StaticTextProperty({ value }, VECTOR_STYLES.LABEL_TEXT); +}; + +describe('syncTextFieldWithMb', () => { + test('Should set with value', async () => { + const dynamicTextProperty = makeProperty('foo'); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', 'foo']]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(''); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts index b0016106b8c31..fb05fa052db21 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts @@ -18,7 +18,9 @@ export class StaticTextProperty extends StaticStyleProperty if (this.getOptions().value.length) { mbMap.setLayoutProperty(mbLayerId, 'text-field', this.getOptions().value); } else { - mbMap.setLayoutProperty(mbLayerId, 'text-field', null); + if (typeof mbMap.getLayoutProperty(mbLayerId, 'text-field') !== 'undefined') { + mbMap.setLayoutProperty(mbLayerId, 'text-field', undefined); + } } } } From ad5f83a36230abeff79d01bfff0a104f5fd615d2 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 5 Apr 2021 13:49:54 -0400 Subject: [PATCH 008/131] [App Search] Added Sample Response section to Result Settings (#95971) --- .../result_settings/result_settings.tsx | 4 +- .../non_text_fields_body.tsx | 5 + .../text_fields_body.tsx | 13 ++ .../result_settings/sample_response/index.ts | 8 + .../sample_response/sample_response.test.tsx | 75 ++++++ .../sample_response/sample_response.tsx | 72 ++++++ .../sample_response_logic.test.ts | 214 ++++++++++++++++++ .../sample_response/sample_response_logic.ts | 100 ++++++++ .../components/result_settings/types.ts | 4 + .../routes/app_search/result_settings.test.ts | 44 ++++ .../routes/app_search/result_settings.ts | 18 ++ 11 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 38db5c60e98a9..7f4373835f8d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -17,6 +17,8 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; + import { ResultSettingsLogic } from '.'; interface Props { @@ -40,7 +42,7 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { -
TODO
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx index 145654be20461..dc91b5039a3c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ResultSettingsLogic } from '..'; import { FieldResultSetting } from '../types'; @@ -33,6 +34,10 @@ export const NonTextFieldsBody: React.FC = () => { { { { { + const actions = { + queryChanged: jest.fn(), + getSearchResults: jest.fn(), + }; + + const values = { + reducedServerResultFields: {}, + query: 'foo', + response: { + bar: 'baz', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders a text box with the current user "query" value from state', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('updates the "query" value in state when a user updates the text in the text box', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } }); + expect(actions.queryChanged).toHaveBeenCalledWith('bar'); + }); + + it('will call getSearchResults with the current value of query and reducedServerResultFields in a useEffect, which updates the displayed response', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('renders the response from the given user "query" in a code block', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('{\n "bar": "baz"\n}'); + }); + + it('renders a plain old string in the code block if the response is a string', () => { + setMockValues({ + response: 'No results.', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('No results.'); + }); + + it('will not render a code block at all if there is no response yet', () => { + setMockValues({ + response: null, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx new file mode 100644 index 0000000000000..ae91b9648356c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiCodeBlock, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +import { SampleResponseLogic } from './sample_response_logic'; + +export const SampleResponse: React.FC = () => { + const { reducedServerResultFields } = useValues(ResultSettingsLogic); + + const { query, response } = useValues(SampleResponseLogic); + const { queryChanged, getSearchResults } = useActions(SampleResponseLogic); + + useEffect(() => { + getSearchResults(query, reducedServerResultFields); + }, [query, reducedServerResultFields]); + + return ( + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponseTitle', + { defaultMessage: 'Sample response' } + )} +

+
+
+ + {/* TODO */} + +
+ + queryChanged(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.inputPlaceholder', + { defaultMessage: 'Type a search query to test a response...' } + )} + data-test-subj="ResultSettingsQuerySampleResponse" + /> + + {!!response && ( + + {typeof response === 'string' ? response : JSON.stringify(response, null, 2)} + + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts new file mode 100644 index 0000000000000..79379306c1618 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockHttpValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { SampleResponseLogic } from './sample_response_logic'; + +describe('SampleResponseLogic', () => { + const { mount } = new LogicMounter(SampleResponseLogic); + const { http } = mockHttpValues; + + const DEFAULT_VALUES = { + query: '', + response: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + }); + }); + + describe('actions', () => { + describe('queryChanged', () => { + it('updates the query', () => { + mount({ + query: '', + }); + + SampleResponseLogic.actions.queryChanged('foo'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('getSearchResultsSuccess', () => { + it('sets the response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsSuccess({}); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: {}, + }); + }); + }); + + describe('getSearchResultsFailure', () => { + it('sets a string response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsFailure('An error occured.'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: 'An error occured.', + }); + }); + }); + }); + + describe('listeners', () => { + describe('getSearchResults', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('makes a search API request and calls getSearchResultsSuccess with the first result of the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [ + { id: { raw: 'foo' }, _meta: {} }, + { id: { raw: 'bar' }, _meta: {} }, + { id: { raw: 'baz' }, _meta: {} }, + ], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith({ + // Note that the _meta field was stripped from the result + id: { raw: 'foo' }, + }); + }); + + it('calls getSearchResultsSuccess with a "No Results." message if there are no results', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith( + 'No results.' + ); + }); + + it('handles 500 errors by setting a generic error response and showing a flash message error', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + const error = { + response: { + status: 500, + }, + }; + + http.post.mockReturnValueOnce(Promise.reject(error)); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('handles 400 errors by setting the response, but does not show a flash error message', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + body: { + attributes: { + errors: ['A validation error occurred.'], + }, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith({ + errors: ['A validation error occurred.'], + }); + }); + + it('sets a generic message on a 400 error if no custom message is provided in the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('does nothing if an empty object is passed for the resultFields parameter', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + SampleResponseLogic.actions.getSearchResults('foo', {}); + + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts new file mode 100644 index 0000000000000..808a7ec9c65dc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { SampleSearchResponse, ServerFieldResultSettingObject } from '../types'; + +const NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.noResultsMessage', + { defaultMessage: 'No results.' } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.errorMessage', + { defaultMessage: 'An error occured.' } +); + +interface SampleResponseValues { + query: string; + response: SampleSearchResponse | string | null; +} + +interface SampleResponseActions { + queryChanged: (query: string) => { query: string }; + getSearchResultsSuccess: ( + response: SampleSearchResponse | string + ) => { response: SampleSearchResponse | string }; + getSearchResultsFailure: (response: string) => { response: string }; + getSearchResults: ( + query: string, + resultFields: ServerFieldResultSettingObject + ) => { query: string; resultFields: ServerFieldResultSettingObject }; +} + +export const SampleResponseLogic = kea>({ + path: ['enterprise_search', 'app_search', 'sample_response_logic'], + actions: { + queryChanged: (query) => ({ query }), + getSearchResultsSuccess: (response) => ({ response }), + getSearchResultsFailure: (response) => ({ response }), + getSearchResults: (query, resultFields) => ({ query, resultFields }), + }, + reducers: { + query: ['', { queryChanged: (_, { query }) => query }], + response: [ + null, + { + getSearchResultsSuccess: (_, { response }) => response, + getSearchResultsFailure: (_, { response }) => response, + }, + ], + }, + listeners: ({ actions }) => ({ + getSearchResults: async ({ query, resultFields }, breakpoint) => { + if (Object.keys(resultFields).length < 1) return; + await breakpoint(250); + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/sample_response_search`; + + try { + const response = await http.post(url, { + body: JSON.stringify({ + query, + result_fields: resultFields, + }), + }); + + const result = response.results?.[0]; + actions.getSearchResultsSuccess( + result ? { ...result, _meta: undefined } : NO_RESULTS_MESSAGE + ); + } catch (e) { + if (e.response.status >= 500) { + // 4XX Validation errors are expected, as a user could enter something like 2 as a size, which is out of valid range. + // In this case, we simply render the message from the server as the response. + // + // 5xx Server errors are unexpected, and need to be reported in a flash message. + flashAPIErrors(e); + actions.getSearchResultsFailure(ERROR_MESSAGE); + } else { + actions.getSearchResultsFailure(e.body?.attributes || ERROR_MESSAGE); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 96bf277314a7b..18843112f46bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FieldValue } from '../result/types'; + export enum OpenModal { None, ConfirmResetModal, @@ -35,3 +37,5 @@ export interface FieldResultSetting { } export type FieldResultSettingObject = Record; + +export type SampleSearchResponse = Record; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index 8d1a7e3ead37b..e38380d60c6e9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -88,4 +88,48 @@ describe('result settings routes', () => { }); }); }); + + describe('POST /api/app_search/engines/{name}/sample_response_search', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/sample_response_search', + }); + + beforeEach(() => { + registerResultSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'test', + result_fields: resultFields, + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/sample_response_search', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + query: 'test', + result_fields: resultFields, + }, + }; + mockRouter.shouldValidate(request); + }); + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index 38cb4aa922738..b091ae7a539c2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -45,4 +45,22 @@ export function registerResultSettingsRoutes({ path: '/as/engines/:engineName/result_settings', }) ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/sample_response_search', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + query: schema.string(), + result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/sample_response_search', + }) + ); } From e457f212c4e513b467fc9fd540b76d4a6949111f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 5 Apr 2021 20:59:26 +0200 Subject: [PATCH 009/131] Revert "TS Incremental build exclude test files (#95610)" (#96223) This reverts commit b6e582c53ebb9c496c232408066b128d2ca2f92c. --- packages/kbn-storybook/tsconfig.json | 2 +- src/core/tsconfig.json | 2 +- src/dev/typescript/project.ts | 37 ++------ src/plugins/advanced_settings/tsconfig.json | 2 +- src/plugins/apm_oss/tsconfig.json | 2 +- src/plugins/bfetch/tsconfig.json | 2 +- src/plugins/charts/tsconfig.json | 2 +- src/plugins/console/tsconfig.json | 2 +- src/plugins/dashboard/tsconfig.json | 2 +- .../search/expressions/exists_filter.test.ts | 2 +- .../search/expressions/kibana_filter.test.ts | 2 +- .../search/expressions/phrase_filter.test.ts | 2 +- .../search/expressions/range_filter.test.ts | 2 +- src/plugins/data/tsconfig.json | 2 +- src/plugins/dev_tools/tsconfig.json | 2 +- src/plugins/discover/tsconfig.json | 2 +- src/plugins/embeddable/tsconfig.json | 2 +- src/plugins/es_ui_shared/tsconfig.json | 2 +- src/plugins/expressions/common/mocks.ts | 2 - src/plugins/expressions/common/util/index.ts | 1 + src/plugins/expressions/tsconfig.json | 2 +- src/plugins/home/tsconfig.json | 2 +- .../index_pattern_field_editor/tsconfig.json | 2 +- .../index_pattern_management/tsconfig.json | 2 +- src/plugins/input_control_vis/tsconfig.json | 2 +- src/plugins/inspector/tsconfig.json | 2 +- src/plugins/kibana_legacy/tsconfig.json | 2 +- src/plugins/kibana_overview/tsconfig.json | 2 +- src/plugins/kibana_react/tsconfig.json | 2 +- .../kibana_usage_collection/tsconfig.json | 2 +- src/plugins/kibana_utils/tsconfig.json | 2 +- src/plugins/legacy_export/tsconfig.json | 2 +- src/plugins/management/tsconfig.json | 2 +- src/plugins/maps_ems/tsconfig.json | 2 +- src/plugins/maps_legacy/tsconfig.json | 2 +- src/plugins/navigation/tsconfig.json | 2 +- src/plugins/newsfeed/tsconfig.json | 2 +- src/plugins/presentation_util/tsconfig.json | 2 +- src/plugins/region_map/tsconfig.json | 2 +- src/plugins/saved_objects/tsconfig.json | 2 +- .../saved_objects_management/tsconfig.json | 2 +- .../saved_objects_tagging_oss/tsconfig.json | 2 +- src/plugins/security_oss/tsconfig.json | 2 +- src/plugins/share/tsconfig.json | 2 +- src/plugins/spaces_oss/tsconfig.json | 2 +- src/plugins/telemetry/tsconfig.json | 2 +- .../tsconfig.json | 2 +- .../tsconfig.json | 2 +- src/plugins/tile_map/tsconfig.json | 2 +- src/plugins/timelion/tsconfig.json | 2 +- src/plugins/ui_actions/tsconfig.json | 2 +- src/plugins/url_forwarding/tsconfig.json | 2 +- src/plugins/usage_collection/tsconfig.json | 2 +- src/plugins/vis_default_editor/tsconfig.json | 2 +- src/plugins/vis_type_markdown/tsconfig.json | 2 +- src/plugins/vis_type_metric/tsconfig.json | 2 +- src/plugins/vis_type_table/tsconfig.json | 2 +- src/plugins/vis_type_tagcloud/tsconfig.json | 2 +- src/plugins/vis_type_timelion/tsconfig.json | 2 +- src/plugins/vis_type_timeseries/tsconfig.json | 2 +- src/plugins/vis_type_vega/tsconfig.json | 2 +- src/plugins/vis_type_vislib/tsconfig.json | 2 +- src/plugins/vis_type_xy/tsconfig.json | 2 +- src/plugins/visualizations/tsconfig.json | 2 +- src/plugins/visualize/tsconfig.json | 2 +- tsconfig.base.json | 3 +- tsconfig.json | 73 ---------------- tsconfig.project.json | 65 -------------- x-pack/plugins/actions/tsconfig.json | 2 +- x-pack/plugins/alerting/tsconfig.json | 2 +- x-pack/plugins/apm/e2e/tsconfig.json | 2 +- x-pack/plugins/apm/ftr_e2e/tsconfig.json | 4 +- .../apm/scripts/optimize-tsconfig/optimize.js | 2 +- .../apm/scripts/optimize-tsconfig/paths.js | 2 +- x-pack/plugins/apm/tsconfig.json | 2 +- x-pack/plugins/banners/tsconfig.json | 2 +- x-pack/plugins/beats_management/tsconfig.json | 2 +- .../canvas/storybook/addon/tsconfig.json | 2 +- x-pack/plugins/canvas/tsconfig.json | 2 +- x-pack/plugins/cloud/tsconfig.json | 2 +- .../plugins/console_extensions/tsconfig.json | 2 +- .../cross_cluster_replication/tsconfig.json | 2 +- .../plugins/dashboard_enhanced/tsconfig.json | 2 +- x-pack/plugins/dashboard_mode/tsconfig.json | 2 +- x-pack/plugins/data_enhanced/tsconfig.json | 2 +- .../plugins/discover_enhanced/tsconfig.json | 2 +- .../drilldowns/url_drilldown/tsconfig.json | 2 +- .../plugins/embeddable_enhanced/tsconfig.json | 2 +- .../encrypted_saved_objects/tsconfig.json | 2 +- .../plugins/enterprise_search/tsconfig.json | 2 +- x-pack/plugins/event_log/tsconfig.json | 2 +- x-pack/plugins/features/tsconfig.json | 2 +- x-pack/plugins/file_upload/tsconfig.json | 2 +- x-pack/plugins/fleet/tsconfig.json | 2 +- x-pack/plugins/global_search/tsconfig.json | 2 +- .../plugins/global_search_bar/tsconfig.json | 2 +- .../global_search_providers/tsconfig.json | 2 +- x-pack/plugins/graph/tsconfig.json | 4 +- x-pack/plugins/grokdebugger/tsconfig.json | 2 +- .../index_lifecycle_management/tsconfig.json | 3 +- x-pack/plugins/index_management/tsconfig.json | 4 +- .../infra/public/utils/enzyme_helpers.tsx | 87 +++++++++++++++++++ .../queries/metrics_hosts_anomalies.ts | 2 +- .../infra_ml/queries/metrics_k8s_anomalies.ts | 2 +- x-pack/plugins/infra/tsconfig.json | 2 +- x-pack/plugins/ingest_pipelines/tsconfig.json | 3 +- x-pack/plugins/lens/tsconfig.json | 4 +- .../plugins/license_management/tsconfig.json | 2 +- x-pack/plugins/licensing/tsconfig.json | 2 +- x-pack/plugins/logstash/tsconfig.json | 2 +- .../components/validated_number_input.tsx | 4 +- x-pack/plugins/maps/tsconfig.json | 2 +- .../routes/apidoc_scripts/tsconfig.json | 2 +- x-pack/plugins/ml/tsconfig.json | 2 +- x-pack/plugins/monitoring/tsconfig.json | 2 +- x-pack/plugins/observability/tsconfig.json | 2 +- x-pack/plugins/osquery/tsconfig.json | 2 +- x-pack/plugins/painless_lab/tsconfig.json | 2 +- x-pack/plugins/remote_clusters/tsconfig.json | 2 +- x-pack/plugins/reporting/tsconfig.json | 2 +- x-pack/plugins/rollup/tsconfig.json | 2 +- x-pack/plugins/runtime_fields/tsconfig.json | 2 +- .../saved_objects_tagging/tsconfig.json | 2 +- x-pack/plugins/searchprofiler/tsconfig.json | 2 +- x-pack/plugins/security/tsconfig.json | 2 +- .../security_solution/cypress/tsconfig.json | 2 +- x-pack/plugins/snapshot_restore/tsconfig.json | 4 +- x-pack/plugins/spaces/tsconfig.json | 2 +- x-pack/plugins/stack_alerts/tsconfig.json | 2 +- .../server/monitoring/workload_statistics.ts | 3 +- x-pack/plugins/task_manager/tsconfig.json | 2 +- .../telemetry_collection_xpack/tsconfig.json | 2 +- x-pack/plugins/transform/tsconfig.json | 2 +- .../plugins/triggers_actions_ui/tsconfig.json | 2 +- .../plugins/ui_actions_enhanced/tsconfig.json | 2 +- .../plugins/upgrade_assistant/tsconfig.json | 2 +- .../certificates/cert_monitors.test.tsx | 2 +- .../certificates/cert_search.test.tsx | 2 +- .../certificates/cert_status.test.tsx | 2 +- .../certificates/certificates_list.test.tsx | 2 +- .../certificates/fingerprint_col.test.tsx | 2 +- .../common/charts/duration_charts.test.tsx | 2 +- .../common/charts/monitor_bar_series.test.tsx | 3 +- .../common/header/page_header.test.tsx | 2 +- .../components/common/monitor_tags.test.tsx | 2 +- .../common/uptime_date_picker.test.tsx | 4 +- .../monitor/ml/ml_integerations.test.tsx | 2 +- .../monitor/ml/ml_manage_job.test.tsx | 2 +- .../monitor/monitor_charts.test.tsx | 2 +- .../components/monitor/monitor_title.test.tsx | 2 +- .../monitor_status.bar.test.tsx | 2 +- .../status_details/ssl_certificate.test.tsx | 6 +- .../overview/empty_state/empty_state.test.tsx | 2 +- .../columns/enable_alert.test.tsx | 2 +- .../filter_status_button.test.tsx | 3 +- .../monitor_list/monitor_list.test.tsx | 2 +- .../monitor_list_drawer.test.tsx | 2 +- .../monitor_list/status_filter.test.tsx | 4 +- .../settings/certificate_form.test.tsx | 2 +- .../components/settings/indices_form.test.tsx | 2 +- .../public/hooks/use_breadcrumbs.test.tsx | 5 +- .../public/hooks/use_url_params.test.tsx | 3 +- .../plugins/uptime/public/lib/helper/index.ts | 1 + x-pack/plugins/uptime/public/lib/index.ts | 9 ++ .../uptime/public/pages/certificates.test.tsx | 2 +- .../uptime/public/pages/monitor.test.tsx | 2 +- .../uptime/public/pages/not_found.test.tsx | 2 +- .../uptime/public/pages/overview.test.tsx | 2 +- x-pack/plugins/uptime/tsconfig.json | 3 +- x-pack/plugins/watcher/tsconfig.json | 2 +- x-pack/plugins/xpack_legacy/tsconfig.json | 2 +- 171 files changed, 281 insertions(+), 351 deletions(-) delete mode 100644 tsconfig.project.json create mode 100644 x-pack/plugins/infra/public/utils/enzyme_helpers.tsx create mode 100644 x-pack/plugins/uptime/public/lib/index.ts diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 5c81c9100e601..db10d4630ff9c 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.json", "compilerOptions": { "incremental": false, "outDir": "target", diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index f19e379482d3c..855962070457e 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.project.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 04a5de945619b..8d92284e49637 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -7,7 +7,7 @@ */ import { basename, dirname, relative, resolve } from 'path'; -import { memoize } from 'lodash'; + import { IMinimatch, Minimatch } from 'minimatch'; import { REPO_ROOT } from '@kbn/utils'; @@ -26,10 +26,6 @@ function testMatchers(matchers: IMinimatch[], path: string) { return matchers.some((matcher) => matcher.match(path)); } -const parentProjectFactory = memoize(function (parentConfigPath: string) { - return new Project(parentConfigPath); -}); - export class Project { public directory: string; public name: string; @@ -38,7 +34,6 @@ export class Project { private readonly include: IMinimatch[]; private readonly exclude: IMinimatch[]; - private readonly parent?: Project; constructor( public tsConfigPath: string, @@ -46,16 +41,15 @@ export class Project { ) { this.config = parseTsConfig(tsConfigPath); - const { files, include, exclude = [], extends: extendsPath } = this.config as { + const { files, include, exclude = [] } = this.config as { files?: string[]; include?: string[]; exclude?: string[]; - extends?: string; }; if (files || !include) { throw new Error( - `[${tsConfigPath}]: tsconfig.json files in the Kibana repo must use "include" keys and not "files"` + 'tsconfig.json files in the Kibana repo must use "include" keys and not "files"' ); } @@ -64,30 +58,9 @@ export class Project { this.name = options.name || relative(REPO_ROOT, this.directory) || basename(this.directory); this.include = makeMatchers(this.directory, include); this.exclude = makeMatchers(this.directory, exclude); - - if (extendsPath !== undefined) { - const parentConfigPath = resolve(this.directory, extendsPath); - this.parent = parentProjectFactory(parentConfigPath); - } - } - - public isAbsolutePathSelected(path: string): boolean { - return this.isExcluded(path) ? false : this.isIncluded(path); } - public isExcluded(path: string): boolean { - if (testMatchers(this.exclude, path)) return true; - if (this.parent) { - return this.parent.isExcluded(path); - } - return false; - } - - public isIncluded(path: string): boolean { - if (testMatchers(this.include, path)) return true; - if (this.parent) { - return this.parent.isIncluded(path); - } - return false; + public isAbsolutePathSelected(path: string) { + return testMatchers(this.exclude, path) ? false : testMatchers(this.include, path); } } diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 97a855959903d..4d62e410326b6 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json index ccb123aaec83b..aeb6837c69a99 100644 --- a/src/plugins/apm_oss/tsconfig.json +++ b/src/plugins/apm_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json index 6c01479f1929e..173ff725d07d0 100644 --- a/src/plugins/bfetch/tsconfig.json +++ b/src/plugins/bfetch/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json index 99edb2ffe3c16..a4f65d5937204 100644 --- a/src/plugins/charts/tsconfig.json +++ b/src/plugins/charts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index d9f49036be8f8..34aca5021bac4 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 452208b39af60..dd99119cfb457 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/data/common/search/expressions/exists_filter.test.ts b/src/plugins/data/common/search/expressions/exists_filter.test.ts index 60e8a9c7a09ce..e3b53b2281398 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.test.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { existsFilterFunction } from './exists_filter'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 56a9e1ce660cd..ac8ae55492cc0 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { kibanaFilterFunction } from './kibana_filter'; diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts index 90e471e166f5e..39bd907513a0d 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.test.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { phraseFilterFunction } from './phrase_filter'; diff --git a/src/plugins/data/common/search/expressions/range_filter.test.ts b/src/plugins/data/common/search/expressions/range_filter.test.ts index 129e6bd82e16a..92670f8a044ba 100644 --- a/src/plugins/data/common/search/expressions/range_filter.test.ts +++ b/src/plugins/data/common/search/expressions/range_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { rangeFilterFunction } from './range_filter'; diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index b99a2f6f85904..9c95878af631e 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index f369396b17fbe..c17b2341fd42f 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 96765d76a340b..ec98199c3423e 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index eacfa831ecee5..27a887500fb68 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index 3d102daaf3aaf..9bcda2e0614de 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts index 20bdbca07f008..eaeebd8e53492 100644 --- a/src/plugins/expressions/common/mocks.ts +++ b/src/plugins/expressions/common/mocks.ts @@ -34,5 +34,3 @@ export const createMockExecutionContext = ...extraContext, }; }; - -export { createMockContext } from './util/test_utils'; diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index 5f83d962d5aea..470dfc3c2d436 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -10,3 +10,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; export * from './expressions_inspector_adapter'; +export * from './test_utils'; diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index fe76ba3050a3b..cce71013cefa5 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index 19ab5a8e6efec..b15e1fc011b92 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json index c638fd34c6bbb..559b1aaf0fc26 100644 --- a/src/plugins/index_pattern_field_editor/tsconfig.json +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 3c8fdb1cf6597..37bd3e4aa5bbb 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json index c2f8d8783e822..bef7bc394a6cc 100644 --- a/src/plugins/input_control_vis/tsconfig.json +++ b/src/plugins/input_control_vis/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json index 0e42e577428c6..2a9c41464532c 100644 --- a/src/plugins/inspector/tsconfig.json +++ b/src/plugins/inspector/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_legacy/tsconfig.json b/src/plugins/kibana_legacy/tsconfig.json index 0b3f42cd3b57b..709036c9e82f4 100644 --- a/src/plugins/kibana_legacy/tsconfig.json +++ b/src/plugins/kibana_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index 3396861cb9179..ac3ac109cb35f 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_react/tsconfig.json b/src/plugins/kibana_react/tsconfig.json index 857b8cf83645c..eb9a24ca141f6 100644 --- a/src/plugins/kibana_react/tsconfig.json +++ b/src/plugins/kibana_react/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index 100f1f03955d0..d664d936f6667 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index d9572707e8662..ae5e9b90af807 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json index d6689ea1067db..ec006d492499e 100644 --- a/src/plugins/legacy_export/tsconfig.json +++ b/src/plugins/legacy_export/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index 3423299a53df7..ba3661666631a 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_ems/tsconfig.json b/src/plugins/maps_ems/tsconfig.json index 7f44da00d47a4..b85c3da66b83a 100644 --- a/src/plugins/maps_ems/tsconfig.json +++ b/src/plugins/maps_ems/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json index c600024cc4a74..f757e35f785af 100644 --- a/src/plugins/maps_legacy/tsconfig.json +++ b/src/plugins/maps_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json index bb86142e1c443..07cfe10d7d81f 100644 --- a/src/plugins/navigation/tsconfig.json +++ b/src/plugins/navigation/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 84626b2f3a6a8..66244a22336c7 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index cb39c5fb36f56..37b9380f6f2b9 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json index 385c31e6bd2d6..899611d027465 100644 --- a/src/plugins/region_map/tsconfig.json +++ b/src/plugins/region_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index 46b3f9a5afcb6..d9045b91b9dfa 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index cb74b06179225..99849dea38618 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index ae566d9626895..b0059c71424bf 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index 156e4ffee9d79..530e01a034b00 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 62c0b3739f4b7..985066915f1dd 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json index 96584842ec32b..0cc82d7e5d124 100644 --- a/src/plugins/spaces_oss/tsconfig.json +++ b/src/plugins/spaces_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 40370082f99e2..bdced01d9eb6f 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 8bbc440fb1a54..1bba81769f0dd 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index c6a21733b2d6b..48e40814b8570 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json index 385c31e6bd2d6..899611d027465 100644 --- a/src/plugins/tile_map/tsconfig.json +++ b/src/plugins/tile_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json index bb8a48339d44e..5b96d69a878ea 100644 --- a/src/plugins/timelion/tsconfig.json +++ b/src/plugins/timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json index 89b66d18705c2..a871d7215cdc5 100644 --- a/src/plugins/ui_actions/tsconfig.json +++ b/src/plugins/ui_actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index f1916e4ce5957..8e867a6bad14f 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index b4a0721ef3672..96b2c4d37e17c 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json index 54a84e08224a8..27bb775c2d0e8 100644 --- a/src/plugins/vis_default_editor/tsconfig.json +++ b/src/plugins/vis_default_editor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json index f940c295e7cee..d5ab89b98081b 100644 --- a/src/plugins/vis_type_markdown/tsconfig.json +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json index 8cee918a3dc82..7441848d5a430 100644 --- a/src/plugins/vis_type_metric/tsconfig.json +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index 4f2e80575497b..ccff3c349cf21 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json index f7f3688183a48..18bbad2257466 100644 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json index d29fb25b15315..77f97de28366d 100644 --- a/src/plugins/vis_type_timelion/tsconfig.json +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json index edc2d25b867d1..7b2dd4b608c1c 100644 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index f375a2483e24f..4091dafcbe357 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json index 845628a6b86f9..74bc1440d9dbc 100644 --- a/src/plugins/vis_type_vislib/tsconfig.json +++ b/src/plugins/vis_type_vislib/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_xy/tsconfig.json b/src/plugins/vis_type_xy/tsconfig.json index 7e23b07bd80f6..5cb0bc8d0bc8e 100644 --- a/src/plugins/vis_type_xy/tsconfig.json +++ b/src/plugins/vis_type_xy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 65de6908228b3..d7c5e6a4b4366 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 046202d82d1aa..bc0891f391746 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/tsconfig.base.json b/tsconfig.base.json index c28ed0d8c8750..da4de5ef3712b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -56,6 +56,5 @@ "jest-styled-components", "@testing-library/jest-dom" ] - }, - "include": [] + } } diff --git a/tsconfig.json b/tsconfig.json index 7c06e80858640..40763ede1bbdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,79 +19,6 @@ "x-pack/plugins/cases/**/*", "x-pack/plugins/lists/**/*", "x-pack/plugins/security_solution/**/*", - - // tests - "src/**/*.test.ts", - "src/**/*.test.tsx", - "src/**/integration_tests/*", - "src/**/tests/*", - // mocks - "src/**/__mocks__/*", - "src/**/mock/*", - "src/**/mocks/*", - "src/**/*/mock.ts", - "src/**/*/mocks.ts", - "src/**/*/mocks.tsx", - "src/**/*.mock.ts", - "src/**/*.mock.tsx", - "src/**/*.mocks.ts", - "src/**/*.mocks.tsx", - - // test helpers - "src/**/test_helpers/*", - "src/**/test_utils/*", - "src/**/*/test_utils.ts", - "src/**/*/test_helpers.ts", - "src/**/*/test_helper.tsx", - - // stubs - "src/**/*/stubs.ts", - "src/**/*.stub.ts", - "src/**/*.stories.tsx", - "src/**/*/_mock_handler_arguments.ts", - - // tests - "x-pack/plugins/**/*.test.ts", - "x-pack/plugins/**/*.test.tsx", - "x-pack/plugins/**/test/**/*", - "x-pack/plugins/**/tests/*", - "x-pack/plugins/**/integration_tests/*", - "x-pack/plugins/**/tests_client_integration/*", - "x-pack/plugins/**/__fixtures__/*", - "x-pack/plugins/**/__stories__/*", - "x-pack/plugins/**/__jest__/**/*", - - // mocks - "x-pack/plugins/**/__mocks__/*", - "x-pack/plugins/**/mock/*", - "x-pack/plugins/**/mocks/*", - "x-pack/plugins/**/*/mock.ts", - "x-pack/plugins/**/*/mocks.ts", - "x-pack/plugins/**/*/mocks.tsx", - "x-pack/plugins/**/*.mock.ts", - "x-pack/plugins/**/*.mock.tsx", - "x-pack/plugins/**/*.mocks.ts", - "x-pack/plugins/**/*.mocks.tsx", - - // test helpers - "x-pack/plugins/**/test_helpers/*", - "x-pack/plugins/**/test_utils/*", - "x-pack/plugins/**/*/test_utils.ts", - "x-pack/plugins/**/*/test_helper.tsx", - "x-pack/plugins/**/*/test_helpers.ts", - "x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx", - "x-pack/plugins/uptime/server/lib/requests/helper.ts", - "x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx", - "x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx", - "x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx", - "x-pack/plugins/apm/server/utils/test_helpers.tsx", - "x-pack/plugins/apm/public/utils/testHelpers.tsx", - - // stubs - "x-pack/plugins/**/*/stubs.ts", - "x-pack/plugins/**/*.stub.ts", - "x-pack/plugins/**/*.stories.tsx", - "x-pack/plugins/**/*/_mock_handler_arguments.ts" ], "exclude": [ "x-pack/plugins/security_solution/cypress/**/*" diff --git a/tsconfig.project.json b/tsconfig.project.json deleted file mode 100644 index 174c3fdf0fd54..0000000000000 --- a/tsconfig.project.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true, - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "exclude": [ - // tests - "**/*.test.ts", - "**/*.test.tsx", - "**/integration_tests/*", - "**/test/**/*", - "**/test/*", - "**/tests/*", - "**/tests_client_integration/*", - "**/__fixtures__/*", - "**/__stories__/*", - "**/__jest__/**", - - // mocks - "**/__mocks__/*", - "**/mock/*", - "**/mocks/*", - "**/*/mock.ts", - "**/*/mocks.ts", - "**/*/mocks.tsx", - "**/*.mock.ts", - "**/*.mock.tsx", - "**/*.mocks.ts", - "**/*.mocks.tsx", - - // test helpers - "**/test_helpers/*", - "**/test_utils/*", - "**/*/test_utils.ts", - "**/*/test_helper.tsx", - "**/*/test_helpers.ts", - // x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx - "**/*/test_data.tsx", - "**/*/shared_columns_tests.tsx", - // x-pack/plugins/uptime/server/lib/requests/helper.ts - "**/*/requests/helper.ts", - // x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx - "**/*/rtl_helpers.tsx", - // x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx - "**/*/enzyme_helpers.tsx", - // x-pack/plugins/apm/server/utils/test_helpers.tsx - "**/*/test_helpers.tsx", - // x-pack/plugins/apm/public/utils/testHelpers.tsx - "**/*/testHelpers.tsx", - - // stubs - "**/*/stubs.ts", - "**/*.stub.ts", - "**/*.stories.tsx", - "**/*/_mock_handler_arguments.ts" - ], - "include": [], - "types": [ - "node", - "flot" - ] -} diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 10ebd09235236..d5c1105c99ad0 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 4010688746901..86ab00faeb5ad 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index 0c13dd717991c..c4587349c7ad7 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": ["tmp"], "include": ["./**/*"], "compilerOptions": { diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index f699943a254fa..168801f782607 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": [ "tmp" ], @@ -12,4 +12,4 @@ "node" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index 613435fe186bf..fed938119c4a6 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -22,7 +22,7 @@ const { kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); const { unoptimizeTsConfig } = require('./unoptimize'); async function prepareBaseTsConfig() { - const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.project.json'); + const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.base.json'); const config = json5.parse(await readFile(baseConfigFilename, 'utf-8')); await writeFile( diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index b501ec3a8eedf..dbc207c9e6d26 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -12,7 +12,7 @@ const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), - path.resolve(kibanaRoot, 'tsconfig.project.json'), + path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), ]; diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index ae2085dc24003..ffbf11c23f63a 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json index 6c4c80173208b..85608a8a78ad5 100644 --- a/x-pack/plugins/banners/tsconfig.json +++ b/x-pack/plugins/banners/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/beats_management/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json index 398438712b26b..ad68cc900e638 100644 --- a/x-pack/plugins/beats_management/tsconfig.json +++ b/x-pack/plugins/beats_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json index b115d1c46546c..2ab1856de661a 100644 --- a/x-pack/plugins/canvas/storybook/addon/tsconfig.json +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../../tsconfig.project.json", + "extends": "../../../../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx" diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 679165f0a1b76..487b68ba3542b 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index f6edb9fb7ccae..46e81aa7fa086 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/console_extensions/tsconfig.json b/x-pack/plugins/console_extensions/tsconfig.json index edcd46c4fafc5..5ad28f230a0bb 100644 --- a/x-pack/plugins/console_extensions/tsconfig.json +++ b/x-pack/plugins/console_extensions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json index 156a851abb8db..9c7590b9c2553 100644 --- a/x-pack/plugins/cross_cluster_replication/tsconfig.json +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index f6acdddc6f997..567c390edfa5a 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_mode/tsconfig.json b/x-pack/plugins/dashboard_mode/tsconfig.json index c4a11959ec3e3..6e4ed11ffa7ff 100644 --- a/x-pack/plugins/dashboard_mode/tsconfig.json +++ b/x-pack/plugins/dashboard_mode/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index 5538a2db3e4cd..047b9b06516ba 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 2a055bd0e0710..38a55e557909b 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 99aea16a9aaba..50fe41c49b0c8 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index 32754f2fd5524..6e9eb69585cbc 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 9eae8b7366bea..2b51b313d34fc 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index a4f1c55463e75..6b4c50770b49f 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/event_log/tsconfig.json b/x-pack/plugins/event_log/tsconfig.json index e21dbc93b7b47..9b7cde10da3d6 100644 --- a/x-pack/plugins/event_log/tsconfig.json +++ b/x-pack/plugins/event_log/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json index 11e2dbc8f093f..1260af55fbff6 100644 --- a/x-pack/plugins/features/tsconfig.json +++ b/x-pack/plugins/features/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 8a982f83632aa..887a05af31174 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 66849e017395b..a20d82de3c859 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/global_search/tsconfig.json b/x-pack/plugins/global_search/tsconfig.json index 2571f7c2e2935..2d05328f445df 100644 --- a/x-pack/plugins/global_search/tsconfig.json +++ b/x-pack/plugins/global_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/global_search_bar/tsconfig.json b/x-pack/plugins/global_search_bar/tsconfig.json index 595d55c3c5a0d..266eecc35c84b 100644 --- a/x-pack/plugins/global_search_bar/tsconfig.json +++ b/x-pack/plugins/global_search_bar/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/global_search_providers/tsconfig.json b/x-pack/plugins/global_search_providers/tsconfig.json index 9be9a681ee7c5..f2759954a6845 100644 --- a/x-pack/plugins/global_search_providers/tsconfig.json +++ b/x-pack/plugins/global_search_providers/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/graph/tsconfig.json b/x-pack/plugins/graph/tsconfig.json index ae0d143455d52..741c603e3aae4 100644 --- a/x-pack/plugins/graph/tsconfig.json +++ b/x-pack/plugins/graph/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -27,4 +27,4 @@ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" } ] - } + } \ No newline at end of file diff --git a/x-pack/plugins/grokdebugger/tsconfig.json b/x-pack/plugins/grokdebugger/tsconfig.json index 3e9e2f7870814..51d2d0b6db0ea 100644 --- a/x-pack/plugins/grokdebugger/tsconfig.json +++ b/x-pack/plugins/grokdebugger/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index bf43817cbc407..75bd775a36749 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -8,6 +8,7 @@ "declarationMap": true }, "include": [ + "__jest__/**/*", "common/**/*", "public/**/*", "server/**/*", diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json index 0766839a230b9..81a96a77cef83 100644 --- a/x-pack/plugins/index_management/tsconfig.json +++ b/x-pack/plugins/index_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -8,9 +8,11 @@ "declarationMap": true }, "include": [ + "__jest__/**/*", "common/**/*", "public/**/*", "server/**/*", + "test/**/*", "../../../typings/**/*", ], "references": [ diff --git a/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx b/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx new file mode 100644 index 0000000000000..33fbbd03d790a --- /dev/null +++ b/x-pack/plugins/infra/public/utils/enzyme_helpers.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act as reactAct } from 'react-dom/test-utils'; +/** + * A wrapper object to provide access to the state of a hook under test and to + * enable interaction with that hook. + */ +interface ReactHookWrapper { + /* Ensures that async React operations have settled before and after the + * given actor callback is called. The actor callback arguments provide easy + * access to the last hook value and allow for updating the arguments passed + * to the hook body to trigger reevaluation. + */ + act: (actor: (lastHookValue: HookValue, setArgs: (args: Args) => void) => void) => void; + /* The enzyme wrapper around the test component. */ + component: ReactWrapper; + /* The most recent value return the by test harness of the hook. */ + getLastHookValue: () => HookValue; + /* The jest Mock function that receives the hook values for introspection. */ + hookValueCallback: jest.Mock; +} + +/** + * Allows for execution of hooks inside of a test component which records the + * returned values. + * + * @param body A function that calls the hook and returns data derived from it + * @param WrapperComponent A component that, if provided, will be wrapped + * around the test component. This can be useful to provide context values. + * @return {ReactHookWrapper} An object providing access to the hook state and + * functions to interact with it. + */ +export const mountHook = ( + body: (args: Args) => HookValue, + WrapperComponent?: React.ComponentType, + initialArgs: Args = {} as Args +): ReactHookWrapper => { + const hookValueCallback = jest.fn(); + let component!: ReactWrapper; + + const act: ReactHookWrapper['act'] = (actor) => { + reactAct(() => { + actor(getLastHookValue(), (args: Args) => component.setProps(args)); + component.update(); + }); + }; + + const getLastHookValue = () => { + const calls = hookValueCallback.mock.calls; + if (calls.length <= 0) { + throw Error('No recent hook value present.'); + } + return calls[calls.length - 1][0]; + }; + + const HookComponent = (props: Args) => { + hookValueCallback(body(props)); + return null; + }; + const TestComponent: React.FunctionComponent = (args) => + WrapperComponent ? ( + + + + ) : ( + + ); + + reactAct(() => { + component = mount(); + }); + + return { + act, + component, + getLastHookValue, + hookValueCallback, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index b9bfcd302fb83..ab50986c3b3d5 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -84,7 +84,7 @@ export const createMetricsHostsAnomaliesQuery = ({ const sortOptions = [ { [sortToMlFieldMap[field]]: querySortDirection }, { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker - ] as const; + ]; const resultsQuery = { ...defaultRequestParameters, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 8dadcfe87c6f9..8fb8df5eef3d7 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -83,7 +83,7 @@ export const createMetricsK8sAnomaliesQuery = ({ const sortOptions = [ { [sortToMlFieldMap[field]]: querySortDirection }, { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker - ] as const; + ]; const resultsQuery = { ...defaultRequestParameters, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index b684e1866282f..765af7974a2f1 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/ingest_pipelines/tsconfig.json b/x-pack/plugins/ingest_pipelines/tsconfig.json index 5917b94caf76b..a248bc9f337fe 100644 --- a/x-pack/plugins/ingest_pipelines/tsconfig.json +++ b/x-pack/plugins/ingest_pipelines/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -11,6 +11,7 @@ "common/**/*", "public/**/*", "server/**/*", + "__jest__/**/*", "../../../typings/**/*" ], "references": [ diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 69ef52535dd9d..134f0b4185b84 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -38,4 +38,4 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json"}, { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, ] - } + } \ No newline at end of file diff --git a/x-pack/plugins/license_management/tsconfig.json b/x-pack/plugins/license_management/tsconfig.json index 925049ca924cf..e6cb0101ee838 100644 --- a/x-pack/plugins/license_management/tsconfig.json +++ b/x-pack/plugins/license_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/licensing/tsconfig.json b/x-pack/plugins/licensing/tsconfig.json index 2d744e57c1895..6118bcd81d342 100644 --- a/x-pack/plugins/licensing/tsconfig.json +++ b/x-pack/plugins/licensing/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json index 6430248e46396..6f21cfdb0b191 100644 --- a/x-pack/plugins/logstash/tsconfig.json +++ b/x-pack/plugins/logstash/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/maps/public/components/validated_number_input.tsx b/x-pack/plugins/maps/public/components/validated_number_input.tsx index 942f31074000f..cd525cf1ee2c9 100644 --- a/x-pack/plugins/maps/public/components/validated_number_input.tsx +++ b/x-pack/plugins/maps/public/components/validated_number_input.tsx @@ -6,8 +6,8 @@ */ import React, { Component, ChangeEvent, ReactNode } from 'react'; -import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; -import type { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; +// @ts-expect-error +import { EuiFieldNumber, EuiFormRow, EuiFormRowDisplayKeys } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 59af94bea0b68..1b74b7ee7566a 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index 599dee1e56c0f..6d01a853698b8 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../../../tsconfig.project.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "target": "es6", diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index b1135b867dc08..6b396b1c59642 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index b1999101f7c12..d0fb7e1a88dcf 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index cc6e298795e4a..f55ae640a8026 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 03c9e451f3b52..291b0f7c607cf 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/painless_lab/tsconfig.json b/x-pack/plugins/painless_lab/tsconfig.json index 2519206b0fcdb..a869b21e06d4d 100644 --- a/x-pack/plugins/painless_lab/tsconfig.json +++ b/x-pack/plugins/painless_lab/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json index b48933bc9f1ec..0bee6300cf0b2 100644 --- a/x-pack/plugins/remote_clusters/tsconfig.json +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 4f252743ed078..88e8d343f4700 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index bf589c62713d6..9b994d1710ffc 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/runtime_fields/tsconfig.json b/x-pack/plugins/runtime_fields/tsconfig.json index e1ad141f1c702..a1efe4c9cf2dd 100644 --- a/x-pack/plugins/runtime_fields/tsconfig.json +++ b/x-pack/plugins/runtime_fields/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/saved_objects_tagging/tsconfig.json b/x-pack/plugins/saved_objects_tagging/tsconfig.json index 5c37481f982d9..d00156ad1277c 100644 --- a/x-pack/plugins/saved_objects_tagging/tsconfig.json +++ b/x-pack/plugins/saved_objects_tagging/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json index 57cd882422b39..f8ac3a61f7812 100644 --- a/x-pack/plugins/searchprofiler/tsconfig.json +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 4ace497dbd5ad..6c3fd1851a8cb 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index bd8d9aa058bc3..270d877a362a6 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": [], "include": [ "./**/*" diff --git a/x-pack/plugins/snapshot_restore/tsconfig.json b/x-pack/plugins/snapshot_restore/tsconfig.json index c496847e4dd9b..39beda02977e1 100644 --- a/x-pack/plugins/snapshot_restore/tsconfig.json +++ b/x-pack/plugins/snapshot_restore/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -8,9 +8,11 @@ "declarationMap": true }, "include": [ + "__jest__/**/*", "common/**/*", "public/**/*", "server/**/*", + "test/**/*", "../../../typings/**/*", ], "references": [ diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 4c67cbe8912bd..95fbecaa90936 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 97eb9a9a05b86..c83935945c67b 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 70b4ba55c2393..c79b310822c3e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -56,7 +56,7 @@ export interface WorkloadAggregation { scheduleDensity: { range: { field: string; - ranges: [{ from: number; to: number }]; + ranges: [{ from: string; to: string }]; }; aggs: { histogram: { @@ -86,6 +86,7 @@ export interface WorkloadAggregation { // The type of a bucket in the scheduleDensity range aggregation type ScheduleDensityResult = AggregationResultOf< + // @ts-expect-error AggregationRange reqires from: number WorkloadAggregation['aggs']['idleTasks']['aggs']['scheduleDensity'], {} >['buckets'][0]; diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 95a098e54619e..a72b678da1f7c 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 80488bd74617d..476f5926f757a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 30da887cc1c43..2717f92c7a4df 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index b7a63d7043f49..8202449b22298 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json index 1513669cdc1ad..39318770126e5 100644 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ b/x-pack/plugins/ui_actions_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 08e45bebf125b..0d65c8ddd8fed 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx index 719c90574b088..5dfc11837b72d 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { CertMonitors } from './cert_monitors'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; describe('CertMonitors', () => { const certMons = [ diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx index a0166fc573754..a991634de22a6 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { CertificateSearch } from './cert_search'; describe('CertificatesSearch', () => { diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx index 999d76f690867..e331a6e5c34fe 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { CertStatus } from './cert_status'; import * as redux from 'react-redux'; import moment from 'moment'; diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx index 8ae0cdb791d9b..ec6a5d91a67c3 100644 --- a/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../lib'; import { CertificateList, CertSort } from './certificates_list'; describe('CertificateList', () => { diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx index 550b7f75623f0..1affd1f990f90 100644 --- a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { FingerprintCol } from './fingerprint_col'; import moment from 'moment'; diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx index 72b1145a9f34e..d7ae92a0e7654 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import DateMath from '@elastic/datemath'; import { DurationChartComponent } from './duration_chart'; import { MonitorDurationResult } from '../../../../common/types'; -import { shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../../lib'; describe('MonitorCharts component', () => { let dateMathSpy: any; diff --git a/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx index b11595eafae4f..792b357b3baba 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { MonitorBarSeries, MonitorBarSeriesProps } from './monitor_bar_series'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../../lib/helper/helper_with_redux'; +import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib'; import { HistogramPoint } from '../../../../common/runtime_types'; describe('MonitorBarSeries component', () => { diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx index bede71b8ba03d..6e04648a817f0 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import moment from 'moment'; import { PageHeader } from './page_header'; import { Ping } from '../../../../common/runtime_types'; -import { renderWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../../lib'; import { mockReduxHooks } from '../../../lib/helper/test_helpers'; describe('PageHeader', () => { diff --git a/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx b/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx index 63465aefcdd43..fdb5498969d39 100644 --- a/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MonitorTags } from './monitor_tags'; import * as hooks from '../../hooks/use_url_params'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; describe('MonitorTags component', () => { const summaryPing = { diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx index d433e7fccd1b8..4bfe7de33cba5 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx @@ -10,9 +10,9 @@ import { UptimeDatePicker } from './uptime_date_picker'; import { renderWithRouter, shallowWithRouter, + MountWithReduxProvider, mountWithRouterRedux, -} from '../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../lib/helper/helper_with_redux'; +} from '../../lib'; import { UptimeStartupPluginsContextProvider } from '../../contexts'; import { startPlugins } from '../../lib/__mocks__/uptime_plugin_start_mock'; import { ClientPluginsStart } from '../../apps/plugin'; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx index 16d96148af340..f29be50633fab 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MLIntegrationComponent } from './ml_integeration'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; import * as redux from 'react-redux'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { coreMock } from 'src/core/public/mocks'; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx index 6bff0b61d7349..15a537a49ccf3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { coreMock } from 'src/core/public/mocks'; import { ManageMLJobComponent } from './manage_ml_job'; import * as redux from 'react-redux'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; const core = coreMock.createStart(); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx index a1be391833bc3..3f107581c1eea 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import DateMath from '@elastic/datemath'; import { MonitorCharts } from './monitor_charts'; -import { shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../lib'; describe('MonitorCharts component', () => { let dateMathSpy: any; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx index 682be99b9b418..dabc0021898eb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import * as reactRouterDom from 'react-router-dom'; import { Ping } from '../../../common/runtime_types'; import { MonitorPageTitle } from './monitor_title'; -import { renderWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../lib'; import { mockReduxHooks } from '../../lib/helper/test_helpers'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx index ba0853b5b1b60..af3c47b9caf30 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MonitorStatusBar } from './status_bar'; import { Ping } from '../../../../common/runtime_types'; import * as redux from 'react-redux'; -import { renderWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../../lib'; import { createMemoryHistory } from 'history'; describe('MonitorStatusBar component', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx index 0cb7ff7168404..03ce292e63621 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx @@ -11,11 +11,7 @@ import { EuiIcon } from '@elastic/eui'; import { Tls } from '../../../../common/runtime_types'; import { MonitorSSLCertificate } from './status_bar'; import * as redux from 'react-redux'; -import { - mountWithRouter, - renderWithRouter, - shallowWithRouter, -} from '../../../lib/helper/enzyme_helpers'; +import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../lib'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('SSL Certificate component', () => { diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx index c751e6a0c24fa..a617ba0db1eb3 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EmptyStateComponent } from './empty_state'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { HttpFetchError, IHttpFetchError } from 'src/core/public'; -import { mountWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { mountWithRouter, shallowWithRouter } from '../../../lib'; describe('EmptyState component', () => { let statesIndexStatus: StatesIndexStatus; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx index a4e7f10d97bfc..a325edc243129 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx @@ -12,7 +12,7 @@ import { mountWithRouterRedux, renderWithRouterRedux, shallowWithRouterRedux, -} from '../../../../lib/helper/enzyme_helpers'; +} from '../../../../lib'; import { EuiPopover, EuiText } from '@elastic/eui'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants'; import { ReactRouterEuiLink } from '../../../common/react_router_helpers'; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.test.tsx index c95e3cd61c5fd..4d0e82dc8a296 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/filter_status_button.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { FilterStatusButton, FilterStatusButtonProps } from './filter_status_button'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../../lib/helper/helper_with_redux'; +import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib'; describe('FilterStatusButton', () => { let props: FilterStatusButtonProps; 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 f7a2ad9a536bd..39f9b20624b63 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 @@ -15,7 +15,7 @@ import { MonitorSummary, } from '../../../../common/runtime_types'; import { MonitorListComponent, noItemsMessage } from './monitor_list'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; import * as redux from 'react-redux'; import moment from 'moment'; import { IHttpFetchError } from '../../../../../../../src/core/public'; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx index 39c6d6bd9215d..d044ad4e6a3a2 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx @@ -9,7 +9,7 @@ import 'jest'; import React from 'react'; import { MonitorListDrawerComponent } from './monitor_list_drawer'; import { MonitorDetails, MonitorSummary, makePing } from '../../../../../common/runtime_types'; -import { shallowWithRouter } from '../../../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../../../lib'; describe('MonitorListDrawer component', () => { let summary: MonitorSummary; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/status_filter.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/status_filter.test.tsx index c2515ab55b126..bbc4e13c9eca2 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/status_filter.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/status_filter.test.tsx @@ -10,8 +10,8 @@ import { mountWithRouter, renderWithRouter, shallowWithRouter, -} from '../../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../../lib/helper/helper_with_redux'; + MountWithReduxProvider, +} from '../../../lib'; import { createMemoryHistory } from 'history'; import { StatusFilter } from './status_filter'; import { FilterStatusButton } from './filter_status_button'; diff --git a/x-pack/plugins/uptime/public/components/settings/certificate_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.test.tsx index 051c4166d0fdd..84c9923bfc419 100644 --- a/x-pack/plugins/uptime/public/components/settings/certificate_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/certificate_form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { CertificateExpirationForm } from './certificate_form'; -import { shallowWithRouter, mountWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter, mountWithRouter } from '../../lib'; describe('CertificateForm', () => { it('shallow renders expected elements for valid props', () => { diff --git a/x-pack/plugins/uptime/public/components/settings/indices_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/indices_form.test.tsx index fc2567ea98c45..67ca142d8a8ea 100644 --- a/x-pack/plugins/uptime/public/components/settings/indices_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/indices_form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { IndicesForm } from './indices_form'; -import { shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../lib'; describe('CertificateForm', () => { it('shallow renders expected elements for valid props', () => { diff --git a/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.test.tsx index 7aeac9706af58..6fc98fbaf1f5b 100644 --- a/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.test.tsx @@ -8,11 +8,10 @@ import { ChromeBreadcrumb } from 'kibana/public'; import React from 'react'; import { Route } from 'react-router-dom'; -import { mountWithRouter } from '../lib/helper/enzyme_helpers'; +import { mountWithRouter } from '../lib'; import { OVERVIEW_ROUTE } from '../../common/constants'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper'; -import { MountWithReduxProvider } from '../lib/helper/helper_with_redux'; +import { UptimeUrlParams, getSupportedUrlParams, MountWithReduxProvider } from '../lib/helper'; import { makeBaseBreadcrumb, useBreadcrumbs } from './use_breadcrumbs'; describe('useBreadcrumbs', () => { diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.test.tsx b/x-pack/plugins/uptime/public/hooks/use_url_params.test.tsx index 31580ec22d48c..3ce112b1cb835 100644 --- a/x-pack/plugins/uptime/public/hooks/use_url_params.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/use_url_params.test.tsx @@ -9,8 +9,7 @@ import DateMath from '@elastic/datemath'; import React, { useState, Fragment } from 'react'; import { useUrlParams, UptimeUrlParamsHook } from './use_url_params'; import { UptimeRefreshContext } from '../contexts'; -import { MountWithReduxProvider } from '../lib/helper/helper_with_redux'; -import { mountWithRouter } from '../lib/helper/enzyme_helpers'; +import { mountWithRouter, MountWithReduxProvider } from '../lib'; import { createMemoryHistory } from 'history'; interface MockUrlParamsComponentProps { diff --git a/x-pack/plugins/uptime/public/lib/helper/index.ts b/x-pack/plugins/uptime/public/lib/helper/index.ts index 6546b5f9ae6c4..2fce3cc0e54dc 100644 --- a/x-pack/plugins/uptime/public/lib/helper/index.ts +++ b/x-pack/plugins/uptime/public/lib/helper/index.ts @@ -10,3 +10,4 @@ export * from './observability_integration'; export { getChartDateLabel } from './charts'; export { seriesHasDownValues } from './series_has_down_values'; export { UptimeUrlParams, getSupportedUrlParams } from './url_params'; +export { MountWithReduxProvider } from './helper_with_redux'; diff --git a/x-pack/plugins/uptime/public/lib/index.ts b/x-pack/plugins/uptime/public/lib/index.ts new file mode 100644 index 0000000000000..9added9af6592 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { MountWithReduxProvider } from './helper'; +export * from './helper/enzyme_helpers'; diff --git a/x-pack/plugins/uptime/public/pages/certificates.test.tsx b/x-pack/plugins/uptime/public/pages/certificates.test.tsx index 1218a1882b3bf..ff5f1afcaa290 100644 --- a/x-pack/plugins/uptime/public/pages/certificates.test.tsx +++ b/x-pack/plugins/uptime/public/pages/certificates.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { shallowWithRouter } from '../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../lib'; import { CertificatesPage } from './certificates'; describe('CertificatesPage', () => { diff --git a/x-pack/plugins/uptime/public/pages/monitor.test.tsx b/x-pack/plugins/uptime/public/pages/monitor.test.tsx index 2664f73d26075..80fcfcc271964 100644 --- a/x-pack/plugins/uptime/public/pages/monitor.test.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MonitorPage } from './monitor'; -import { shallowWithRouter } from '../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../lib'; describe('MonitorPage', () => { it('shallow renders expected elements for valid props', () => { diff --git a/x-pack/plugins/uptime/public/pages/not_found.test.tsx b/x-pack/plugins/uptime/public/pages/not_found.test.tsx index cc9ea1a62cd0f..8d5b20e45303d 100644 --- a/x-pack/plugins/uptime/public/pages/not_found.test.tsx +++ b/x-pack/plugins/uptime/public/pages/not_found.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { shallowWithRouter } from '../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../lib'; import { NotFoundPage } from './not_found'; describe('NotFoundPage', () => { diff --git a/x-pack/plugins/uptime/public/pages/overview.test.tsx b/x-pack/plugins/uptime/public/pages/overview.test.tsx index b4949a84a6e36..cfc140e6b22a3 100644 --- a/x-pack/plugins/uptime/public/pages/overview.test.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { OverviewPageComponent } from './overview'; -import { shallowWithRouter } from '../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../lib'; describe('MonitorPage', () => { const indexPattern = { diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json index 0b98204001753..531ee2ecd8d2b 100644 --- a/x-pack/plugins/uptime/tsconfig.json +++ b/x-pack/plugins/uptime/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -12,6 +12,7 @@ "public/**/*", "public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json", "server/**/*", + "server/lib/requests/__fixtures__/monitor_charts_mock.json", "../../../typings/**/*" ], "references": [ diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json index 41bcb015d2d94..e8dabe8cd40a9 100644 --- a/x-pack/plugins/watcher/tsconfig.json +++ b/x-pack/plugins/watcher/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/xpack_legacy/tsconfig.json b/x-pack/plugins/xpack_legacy/tsconfig.json index fdac21a391313..3bfc78b72cb3e 100644 --- a/x-pack/plugins/xpack_legacy/tsconfig.json +++ b/x-pack/plugins/xpack_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", From 2eae0969cbfe81ceb42b4d5e1146a2f172062ecd Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 5 Apr 2021 13:39:15 -0600 Subject: [PATCH 010/131] [file upload] document file upload privileges and provide actionable UI when failures occur (#95883) * [file upload] document file upload privileges and provide actionable UI when failures occur * doc link * call hasImportPermission * docs tweeks * tslint * Update docs/maps/import-geospatial-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/import-geospatial-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/import-geospatial-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/import-geospatial-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * review feedback * fix bullet list format * clean-up i18n ids * Update docs/maps/import-geospatial-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * documenation review feedback * add period to last privilege bullet item Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/maps/import-geospatial-data.asciidoc | 24 ++++ .../public/doc_links/doc_links_service.ts | 1 + x-pack/plugins/file_upload/common/types.ts | 5 +- .../components/import_complete_view.tsx | 133 +++++++++++++----- .../components/json_upload_and_parse.tsx | 26 ++++ .../file_upload/public/kibana_services.ts | 1 + .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 8 files changed, 154 insertions(+), 44 deletions(-) diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index fb4250368086e..0218bac58815a 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,30 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +[discrete] +[[import-geospatial-privileges]] +=== Security privileges + +The {stack-security-features} provide roles and privileges that control which users can upload files. +You can manage your roles, privileges, and +spaces in **{stack-manage-app}** in {kib}. For more information, see +{ref}/security-privileges.html[Security privileges], +<>, and <>. + +To upload GeoJSON files in {kib} with *Maps*, you must have: + +* The `all` {kib} privilege for *Maps*. +* The `all` {kib} privilege for *Index Pattern Management*. +* The `create` and `create_index` index privileges for destination indices. +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. + +To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: + +* The `manage_pipeline` cluster privilege. +* The `read` {kib} privilege for *Machine Learning*. +* The `machine_learning_admin` or `machine_learning_user` role. + + [discrete] === Upload CSV with latitude and longitude columns diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a946640f58b0d..b179c998f1126 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -216,6 +216,7 @@ export class DocLinksService { }, maps: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, + importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index 0fc59e2b525a8..11cf4ac3615bf 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../src/plugins/data/common'; export interface HasImportPermission { @@ -83,7 +84,9 @@ export interface ImportResponse { pipelineId?: string; docCount: number; failures: ImportFailure[]; - error?: any; + error?: { + error: estypes.ErrorCause; + }; ingestError?: boolean; } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 29aed0cd52f7e..a3bc2ed082b1a 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -7,19 +7,20 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonIcon, EuiCallOut, EuiCopy, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { CodeEditor, KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { getHttp, getUiSettings } from '../kibana_services'; +import { getDocLinks, getHttp, getUiSettings } from '../kibana_services'; import { ImportResults } from '../importer'; const services = { @@ -27,8 +28,10 @@ const services = { }; interface Props { + failedPermissionCheck: boolean; importResults?: ImportResults; indexPatternResp?: object; + indexName: string; } export class ImportCompleteView extends Component { @@ -57,9 +60,12 @@ export class ImportCompleteView extends Component { iconType="copy" color="text" data-test-subj={copyButtonDataTestSubj} - aria-label={i18n.translate('xpack.fileUpload.copyButtonAriaLabel', { - defaultMessage: 'Copy to clipboard', - })} + aria-label={i18n.translate( + 'xpack.fileUpload.importComplete.copyButtonAriaLabel', + { + defaultMessage: 'Copy to clipboard', + } + )} /> )} @@ -90,21 +96,65 @@ export class ImportCompleteView extends Component { } _getStatusMsg() { + if (this.props.failedPermissionCheck) { + return ( + +

+ {i18n.translate('xpack.fileUpload.importComplete.permissionFailureMsg', { + defaultMessage: + 'You do not have permission to create or import data into index "{indexName}".', + values: { indexName: this.props.indexName }, + })} +

+ + {i18n.translate('xpack.fileUpload.importComplete.permission.docLink', { + defaultMessage: 'View file import permissions', + })} + +
+ ); + } + if (!this.props.importResults || !this.props.importResults.success) { - return i18n.translate('xpack.fileUpload.uploadFailureMsg', { - defaultMessage: 'File upload failed.', - }); + const errorMsg = + this.props.importResults && this.props.importResults.error + ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { + defaultMessage: 'Error: {reason}', + values: { reason: this.props.importResults.error.error.reason }, + }) + : ''; + return ( + +

{errorMsg}

+
+ ); } - const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', { - defaultMessage: 'File upload complete: indexed {numFeatures} features.', + const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { + defaultMessage: 'Indexed {numFeatures} features.', values: { numFeatures: this.props.importResults.docCount, }, }); const failedFeaturesMsg = this.props.importResults.failures?.length - ? i18n.translate('xpack.fileUpload.failedFeaturesMsg', { + ? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', { defaultMessage: 'Unable to index {numFailures} features.', values: { numFailures: this.props.importResults.failures.length, @@ -112,47 +162,60 @@ export class ImportCompleteView extends Component { }) : ''; - return `${successMsg} ${failedFeaturesMsg}`; + return ( + +

{`${successMsg} ${failedFeaturesMsg}`}

+
+ ); + } + + _renderIndexManagementMsg() { + return this.props.importResults && this.props.importResults.success ? ( + +

+ + + + +

+
+ ) : null; } render() { return ( - -

{this._getStatusMsg()}

-
+ {this._getStatusMsg()} + {this._renderCodeEditor( this.props.importResults, - i18n.translate('xpack.fileUpload.jsonImport.indexingResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexingResponse', { defaultMessage: 'Import response', }), 'indexRespCopyButton' )} {this._renderCodeEditor( this.props.indexPatternResp, - i18n.translate('xpack.fileUpload.jsonImport.indexPatternResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexPatternResponse', { defaultMessage: 'Index pattern response', }), 'indexPatternRespCopyButton' )} - -
- - - - -
-
+ {this._renderIndexManagementMsg()}
); } diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx index 371d68443bc2c..d73c6e9c5fb3a 100644 --- a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx @@ -16,6 +16,7 @@ import { FileUploadComponentProps } from '../lazy_load_bundle'; import { ImportResults } from '../importer'; import { GeoJsonImporter } from '../importer/geojson_importer'; import { Settings } from '../../common'; +import { hasImportPermission } from '../api'; enum PHASE { CONFIGURE = 'CONFIGURE', @@ -31,6 +32,7 @@ function getWritingToIndexMsg(progress: number) { } interface State { + failedPermissionCheck: boolean; geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE; importStatus: string; importResults?: ImportResults; @@ -45,6 +47,7 @@ export class JsonUploadAndParse extends Component ); } diff --git a/x-pack/plugins/file_upload/public/kibana_services.ts b/x-pack/plugins/file_upload/public/kibana_services.ts index a604136ca34e4..dfe2785e7a2bc 100644 --- a/x-pack/plugins/file_upload/public/kibana_services.ts +++ b/x-pack/plugins/file_upload/public/kibana_services.ts @@ -15,6 +15,7 @@ export function setStartServices(core: CoreStart, plugins: FileUploadStartDepend pluginsStart = plugins; } +export const getDocLinks = () => coreStart.docLinks; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getHttp = () => coreStart.http; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6dc490b4ffc53..dc038c1a7959d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8169,10 +8169,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "インデックス名またはパターンはすでに存在します。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "インデックス名に許可されていない文字が含まれています。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "インデックス名ガイドライン", - "xpack.fileUpload.jsonImport.indexingResponse": "インデックス応答", - "xpack.fileUpload.jsonImport.indexMgmtLink": "インデックス管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "次を使用すると、その他のインデックス修正を行うことができます。\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "インデックスパターン応答", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "データインデックスエラー", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "インデックスパターンエラー", "xpack.fleet.agentBulkActions.clearSelection": "選択した項目をクリア", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 32574690b13f2..117c33a286d88 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8242,10 +8242,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "索引名称或模式已存在。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "索引名称包含非法字符。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "索引名称指引", - "xpack.fileUpload.jsonImport.indexingResponse": "索引响应", - "xpack.fileUpload.jsonImport.indexMgmtLink": "索引管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "要进一步做索引修改,可以使用\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "索引模式响应", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "数据索引错误", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "索引模式错误", "xpack.fleet.agentBulkActions.agentsSelected": "已选择 {count, plural, other {# 个代理}}", From f9917a6c8a9d8cf41642e696e959e07ca4455f28 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 5 Apr 2021 15:28:15 -0500 Subject: [PATCH 011/131] Add Input Controls project configuration (#96238) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/workflows/project-assigner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index d9d2d6d1ddb8b..37d04abda7530 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}], {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} From 5667435c382a49fa92df0a2acbc27fdb07b49192 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 5 Apr 2021 15:32:29 -0500 Subject: [PATCH 012/131] [tech-debt] Remove defunct opacity parameters from EUI shadow functions (#96191) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../canvas/public/components/page_manager/page_manager.scss | 2 +- .../canvas/public/components/workpad_page/workpad_page.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss index 2ed6884542b18..620e0eb113d36 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss @@ -66,7 +66,7 @@ text-decoration: none; .canvasPageManager__pagePreview { - @include euiBottomShadowMedium($opacity: .3); + @include euiBottomShadowMedium; } .canvasPageManager__controls { diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss index 9266273406b84..e770f10927552 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss @@ -1,5 +1,5 @@ .canvasPage { - @include euiBottomShadowFlat($opacity: .4); + @include euiBottomShadowFlat; z-index: initial; position: absolute; top: 0; From 56d9f3d96832f67283ee4a35e306f89fa59ad2f4 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 5 Apr 2021 13:48:00 -0700 Subject: [PATCH 013/131] [App Search] API logs: Add log detail flyout (#96162) * Set up helper for showing JSON request/response bodies * Set up mock API log obj for tests to use * Add ApiLogLogic file for flyout handling * Add ApiLogFlyout component * Update views to load flyout * Update table to open flyout * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts * PR feedback: comments Co-authored-by: Byron Hulcher Co-authored-by: Byron Hulcher Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../api_logs/__mocks__/api_log.mock.ts | 17 +++ .../api_logs/api_log/api_log_flyout.test.tsx | 62 ++++++++ .../api_logs/api_log/api_log_flyout.tsx | 137 ++++++++++++++++++ .../api_logs/api_log/api_log_logic.test.tsx | 57 ++++++++ .../api_logs/api_log/api_log_logic.ts | 44 ++++++ .../components/api_logs/api_log/index.ts | 9 ++ .../components/api_logs/api_logs.tsx | 2 + .../api_logs/api_logs_logic.test.ts | 13 +- .../components/api_logs_table.test.tsx | 3 +- .../api_logs/components/api_logs_table.tsx | 4 +- .../app_search/components/api_logs/index.ts | 1 + .../components/api_logs/utils.test.ts | 21 ++- .../app_search/components/api_logs/utils.ts | 10 ++ .../components/recent_api_logs.tsx | 3 +- 14 files changed, 368 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts new file mode 100644 index 0000000000000..6106cb049c7a1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockApiLog = { + timestamp: '1970-01-01T12:00:00.000Z', + http_method: 'POST', + status: 200, + user_agent: 'Mozilla/5.0', + full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', + request_body: '{"query":"test search"}', + response_body: + '{"meta":{"page":{"current":1,"total_pages":0,"total_results":0,"size":20}},"results":[]}', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx new file mode 100644 index 0000000000000..6bebeee80465c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiBadge } from '@elastic/eui'; + +import { ApiLogFlyout, ApiLogHeading } from './api_log_flyout'; + +describe('ApiLogFlyout', () => { + const values = { + isFlyoutOpen: true, + apiLog: mockApiLog, + }; + const actions = { + closeFlyout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h2').text()).toEqual('Request details'); + expect(wrapper.find(ApiLogHeading).last().dive().find('h3').text()).toEqual('Response body'); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('POST'); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + + wrapper.find(EuiFlyout).simulate('close'); + expect(actions.closeFlyout).toHaveBeenCalled(); + }); + + it('does not render if the flyout is not open', () => { + setMockValues({ ...values, isFlyoutOpen: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if a current apiLog has not been set', () => { + setMockValues({ ...values, apiLog: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx new file mode 100644 index 0000000000000..dd53e997da0f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPortal, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiHealth, + EuiText, + EuiCode, + EuiCodeBlock, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getStatusColor, attemptToFormatJson } from '../utils'; + +import { ApiLogLogic } from './'; + +export const ApiLogFlyout: React.FC = () => { + const { isFlyoutOpen, apiLog } = useValues(ApiLogLogic); + const { closeFlyout } = useActions(ApiLogLogic); + + if (!isFlyoutOpen) return null; + if (!apiLog) return null; + + return ( + + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.flyout.title', { + defaultMessage: 'Request details', + })} +

+
+
+ + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTitle', { + defaultMessage: 'Method', + })} + +
+ {apiLog.http_method} +
+
+ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTitle', { + defaultMessage: 'Status', + })} + + {apiLog.status} + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timestampTitle', { + defaultMessage: 'Timestamp', + })} + + {apiLog.timestamp} + +
+ + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.userAgentTitle', { + defaultMessage: 'User agent', + })} + + + {apiLog.user_agent} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestPathTitle', { + defaultMessage: 'Request path', + })} + + + {apiLog.full_request_path} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestBodyTitle', { + defaultMessage: 'Request body', + })} + + + {attemptToFormatJson(apiLog.request_body)} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.responseBodyTitle', { + defaultMessage: 'Response body', + })} + + + {attemptToFormatJson(apiLog.response_body)} + +
+
+
+ ); +}; + +export const ApiLogHeading: React.FC = ({ children }) => ( + +

{children}

+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx new file mode 100644 index 0000000000000..2b7ca7510e8e1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import { ApiLogLogic } from './'; + +describe('ApiLogLogic', () => { + const { mount } = new LogicMounter(ApiLogLogic); + + const DEFAULT_VALUES = { + isFlyoutOpen: false, + apiLog: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiLogLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('openFlyout', () => { + it('sets isFlyoutOpen to true & sets the current apiLog', () => { + mount({ isFlyoutOpen: false, apiLog: null }); + ApiLogLogic.actions.openFlyout(mockApiLog); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: true, + apiLog: mockApiLog, + }); + }); + }); + + describe('closeFlyout', () => { + it('sets isFlyoutOpen to false & resets the current apiLog', () => { + mount({ isFlyoutOpen: true, apiLog: mockApiLog }); + ApiLogLogic.actions.closeFlyout(); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: false, + apiLog: null, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts new file mode 100644 index 0000000000000..8b7c5f70f605c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { ApiLog } from '../types'; + +interface ApiLogValues { + isFlyoutOpen: boolean; + apiLog: ApiLog | null; +} + +interface ApiLogActions { + openFlyout(apiLog: ApiLog): { apiLog: ApiLog }; + closeFlyout(): void; +} + +export const ApiLogLogic = kea>({ + path: ['enterprise_search', 'app_search', 'api_log_logic'], + actions: () => ({ + openFlyout: (apiLog) => ({ apiLog }), + closeFlyout: true, + }), + reducers: () => ({ + isFlyoutOpen: [ + false, + { + openFlyout: () => true, + closeFlyout: () => false, + }, + ], + apiLog: [ + null, + { + openFlyout: (_, { apiLog }) => apiLog, + closeFlyout: () => null, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts new file mode 100644 index 0000000000000..dcf949d9bf222 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ApiLogFlyout } from './api_log_flyout'; +export { ApiLogLogic } from './api_log_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index 8ca15906783f9..4690911fad772 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -26,6 +26,7 @@ import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; +import { ApiLogFlyout } from './api_log'; import { ApiLogsTable, NewApiEventsPrompt } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; @@ -75,6 +76,7 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts index 7b3ee80668ac7..2eda4c6323fa5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -6,6 +6,7 @@ */ import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { mockApiLog } from './__mocks__/api_log.mock'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; @@ -29,17 +30,7 @@ describe('ApiLogsLogic', () => { }; const MOCK_API_RESPONSE = { - results: [ - { - timestamp: '1970-01-01T12:00:00.000Z', - http_method: 'POST', - status: 200, - user_agent: 'some browser agent string', - full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', - request_body: '{"someMockRequest":"hello"}', - response_body: '{"someMockResponse":"world"}', - }, - ], + results: [mockApiLog, mockApiLog], meta: { page: { current: 1, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx index 99fce81ca348f..768295ec1389c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -53,6 +53,7 @@ describe('ApiLogsTable', () => { }; const actions = { onPaginate: jest.fn(), + openFlyout: jest.fn(), }; beforeEach(() => { @@ -86,7 +87,7 @@ describe('ApiLogsTable', () => { expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click'); - // TODO: API log details flyout + expect(actions.openFlyout).toHaveBeenCalled(); }); it('renders an empty prompt if no items are passed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 8ebcc4350f7fc..5ecf8e1ba3330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -22,6 +22,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { ApiLogLogic } from '../api_log'; import { ApiLogsLogic } from '../index'; import { ApiLog } from '../types'; import { getStatusColor } from '../utils'; @@ -34,6 +35,7 @@ interface Props { export const ApiLogsTable: React.FC = ({ hasPagination }) => { const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); const { onPaginate } = useActions(ApiLogsLogic); + const { openFlyout } = useActions(ApiLogLogic); const columns: Array> = [ { @@ -81,7 +83,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { size="s" className="apiLogDetailButton" data-test-subj="ApiLogsTableDetailsButton" - // TODO: flyout onclick + onClick={() => openFlyout(apiLog)} > {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', { defaultMessage: 'Details', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index 183956e51d8d4..568026dab231f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -7,5 +7,6 @@ export { API_LOGS_TITLE } from './constants'; export { ApiLogsTable, NewApiEventsPrompt } from './components'; +export { ApiLogFlyout } from './api_log'; export { ApiLogs } from './api_logs'; export { ApiLogsLogic } from './api_logs_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts index f9b6dcea2cbf3..ac464e2af353d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getDateString, getStatusColor } from './utils'; +import dedent from 'dedent'; + +import { getDateString, getStatusColor, attemptToFormatJson } from './utils'; describe('getDateString', () => { const mockDate = jest @@ -32,3 +34,20 @@ describe('getStatusColor', () => { expect(getStatusColor(503)).toEqual('danger'); }); }); + +describe('attemptToFormatJson', () => { + it('takes an unformatted JSON string and correctly newlines/indents it', () => { + expect(attemptToFormatJson('{"hello":"world","lorem":{"ipsum":"dolor","sit":"amet"}}')) + .toEqual(dedent`{ + "hello": "world", + "lorem": { + "ipsum": "dolor", + "sit": "amet" + } + }`); + }); + + it('returns the original content if it is not properly formatted JSON', () => { + expect(attemptToFormatJson('{invalid json}')).toEqual('{invalid json}'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts index 3217a1561ce76..7e5f19686f13b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts @@ -19,3 +19,13 @@ export const getStatusColor = (status: number) => { if (status >= 500) color = 'danger'; return color; }; + +export const attemptToFormatJson = (possibleJson: string) => { + try { + // it is JSON, we can format it with newlines/indentation + return JSON.stringify(JSON.parse(possibleJson), null, 2); + } catch { + // if it's not JSON, we return the original content + return possibleJson; + } +}; 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 3686f380407e2..18f27c3a1e834 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 @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; -import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt } from '../../api_logs'; +import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt, ApiLogFlyout } from '../../api_logs'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { DataPanel } from '../../data_panel'; import { generateEnginePath } from '../../engine'; @@ -46,6 +46,7 @@ export const RecentApiLogs: React.FC = () => { hasBorder > + ); }; From 7da1b82b7ee07d94bee2dc2540a7161731e4c513 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Mon, 5 Apr 2021 15:24:22 -0700 Subject: [PATCH 014/131] fixes a skipped management x-pack test (#96178) * fixes a skipped management x-pack test * modified the test to incoroporate the review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../management/users/edit_user/user_form.tsx | 5 ++++ .../functional/apps/security/management.js | 29 ++++++++++--------- .../functional/page_objects/security_page.ts | 5 ++++ 3 files changed, 26 insertions(+), 13 deletions(-) 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 index 8433f54a73343..29d87e31797cc 100644 --- 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 @@ -262,6 +262,7 @@ export const UserForm: FunctionComponent = ({ > = ({ > = ({ > = ({ > = ({ > { - // await PageObjects.security.login('elastic', 'changeme'); await PageObjects.security.initTests(); await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', @@ -43,20 +44,26 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); }); + after(async () => { + await security.role.delete('logstash-readonly'); + await security.user.delete('dashuser', 'new-user'); + await PageObjects.security.forceLogout(); + }); + describe('Security', () => { describe('navigation', () => { it('Can navigate to create user section', async () => { await PageObjects.security.clickElasticsearchUsers(); await PageObjects.security.clickCreateNewUser(); const currentUrl = await browser.getCurrentUrl(); - expect(currentUrl).to.contain(EDIT_USERS_PATH); + expect(currentUrl).to.contain(CREATE_USERS_PATH); }); it('Clicking cancel in create user section brings user back to listing', async () => { await PageObjects.security.clickCancelEditUser(); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(USERS_PATH); - expect(currentUrl).to.not.contain(EDIT_USERS_PATH); + expect(currentUrl).to.not.contain(CREATE_USERS_PATH); }); it('Clicking save in create user section brings user back to listing', async () => { @@ -67,12 +74,11 @@ export default function ({ getService, getPageObjects }) { await testSubjects.setValue('passwordConfirmationInput', '123456'); await testSubjects.setValue('userFormFullNameInput', 'Full User Name'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.clickSaveCreateUser(); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(USERS_PATH); - expect(currentUrl).to.not.contain(EDIT_USERS_PATH); + expect(currentUrl).to.not.contain(CREATE_USERS_PATH); }); it('Can navigate to edit user section', async () => { @@ -143,14 +149,11 @@ export default function ({ getService, getPageObjects }) { 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-readonly'); - - await PageObjects.security.clickSaveEditUser(); - + await PageObjects.security.clickSaveCreateUser(); await PageObjects.settings.navigateTo(); await testSubjects.click('users'); - await PageObjects.settings.clickLinkText('kibana_dashboard_only_user'); + await find.clickByButtonText('logstash-readonly'); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(EDIT_ROLES_PATH); }); diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index f153050018609..97a5c517db794 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -290,6 +290,11 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async clickSaveCreateUser() { + await find.clickByButtonText('Create user'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + async clickSaveEditRole() { const saveButton = await retry.try(() => testSubjects.find('roleFormSaveButton')); await saveButton.moveMouseTo(); From d4b0f92c84ea44c2de3e95385f666ca4c9feb61f Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Mon, 5 Apr 2021 18:02:13 -0500 Subject: [PATCH 015/131] [Dashboard] Fix Lens and TSVB chart tooltip positioning relative to global headers (#94247) --- src/core/public/rendering/_base.scss | 14 ++++++++++++++ src/core/public/rendering/rendering_service.tsx | 1 + .../visualizations/views/timeseries/index.js | 1 + .../vis_type_xy/public/components/xy_settings.tsx | 4 +++- .../public/pie_visualization/render_function.tsx | 2 ++ .../__snapshots__/expression.test.tsx.snap | 7 +++++++ .../lens/public/xy_visualization/expression.tsx | 2 +- 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index de13785a17f5b..ed2d9bc0b3917 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -11,6 +11,16 @@ min-height: 100%; } +#app-fixed-viewport { + pointer-events: none; + visibility: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + .app-wrapper { display: flex; flex-flow: column nowrap; @@ -35,6 +45,10 @@ @mixin kbnAffordForHeader($headerHeight) { padding-top: $headerHeight; + #app-fixed-viewport { + top: $headerHeight; + } + .euiFlyout, .euiCollapsibleNav { top: $headerHeight; diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 843f2a253f33e..787fa475c7d5f 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -52,6 +52,7 @@ export class RenderingService { {chromeHeader}
+
{bannerComponent}
{appComponent}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index a90faea50f22a..2911a9ee5d6e9 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -149,6 +149,7 @@ export const TimeSeries = ({ tooltip={{ snap: true, type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, + boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: tooltipFormatter, }} externalPointerEvents={{ tooltip: { visible: false } }} diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 59bed0060a6a6..8922f512522a0 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -148,13 +148,15 @@ export const XYSettings: FC = ({ : headerValueFormatter && (tooltip.detailedTooltip ? undefined : ({ value }: any) => headerValueFormatter(value)); + const boundary = document.getElementById('app-fixed-viewport') ?? undefined; const tooltipProps: TooltipProps = tooltip.detailedTooltip ? { ...tooltip, + boundary, customTooltip: tooltip.detailedTooltip(headerFormatter), headerFormatter: undefined, } - : { ...tooltip, headerFormatter }; + : { ...tooltip, boundary, headerFormatter }; return (
- } - color="warning" - iconType="alert" - size="s" - /> - )} - - ); -} diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts deleted file mode 100644 index c41b3a2bf5eb5..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Typeahead } from './typehead'; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx deleted file mode 100644 index 2e7dfe990e9c1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { render } from '../../../../../lib/helper/rtl_helpers'; -import { SearchType } from './search_type'; - -describe('Kuery bar search type', () => { - it('can change from simple to kq;', () => { - let kqlSyntax = false; - const setKqlSyntax = jest.fn((val: boolean) => { - kqlSyntax = val; - }); - - const { getByTestId } = render( - - ); - - // open popover to change - fireEvent.click(getByTestId('syntaxChangeToKql')); - - // change syntax - fireEvent.click(getByTestId('toggleKqlSyntax')); - - expect(setKqlSyntax).toHaveBeenCalledWith(true); - expect(setKqlSyntax).toHaveBeenCalledTimes(1); - }); - - it('can change from kql to simple;', () => { - let kqlSyntax = false; - const setKqlSyntax = jest.fn((val: boolean) => { - kqlSyntax = val; - }); - - const { getByTestId } = render( - - ); - - fireEvent.click(getByTestId('syntaxChangeToKql')); - - fireEvent.click(getByTestId('toggleKqlSyntax')); - - expect(setKqlSyntax).toHaveBeenCalledWith(true); - expect(setKqlSyntax).toHaveBeenCalledTimes(1); - }); - - it('clears the query on change to kql', () => { - const setKqlSyntax = jest.fn(); - - const { history } = render(, { - url: '/app/uptime?query=test', - }); - - expect(history?.location.search).toBe(''); - }); - - it('clears the search param on change to simple syntax', () => { - const setKqlSyntax = jest.fn(); - - const { history } = render(, { - url: '/app/uptime?search=test', - }); - - expect(history?.location.search).toBe(''); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx deleted file mode 100644 index af539e1c361a1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { - EuiPopover, - EuiFormRow, - EuiSwitch, - EuiButtonEmpty, - EuiPopoverTitle, - EuiText, - EuiSpacer, - EuiLink, - EuiButtonIcon, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { useUrlParams } from '../../../../../hooks'; -import { - CHANGE_SEARCH_BAR_SYNTAX, - CHANGE_SEARCH_BAR_SYNTAX_SIMPLE, - SYNTAX_OPTIONS_LABEL, -} from '../translations'; - -const BoxesVerticalIcon = euiStyled(EuiButtonIcon)` - padding: 10px 8px 0 8px; - border-radius: 0; - height: 38px; - width: 32px; - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - padding-top: 8px; - padding-bottom: 8px; - cursor: pointer; -`; - -interface Props { - kqlSyntax: boolean; - setKqlSyntax: (val: boolean) => void; -} - -export const SearchType = ({ kqlSyntax, setKqlSyntax }: Props) => { - const { - services: { docLinks }, - } = useKibana(); - - const [getUrlParams, updateUrlParams] = useUrlParams(); - - const { query, search } = getUrlParams(); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const onButtonClick = () => setIsPopoverOpen((prevState) => !prevState); - - const closePopover = () => setIsPopoverOpen(false); - - useEffect(() => { - if (kqlSyntax && query) { - updateUrlParams({ query: '' }); - } - - if (!kqlSyntax && search) { - updateUrlParams({ search: '' }); - } - }, [kqlSyntax, query, search, updateUrlParams]); - - const button = kqlSyntax ? ( - - KQL - - ) : ( - - ); - - return ( - -
- {SYNTAX_OPTIONS_LABEL} - -

- -

-
- - - setKqlSyntax(!kqlSyntax)} - data-test-subj="toggleKqlSyntax" - /> - -
-
- ); -}; - -const KqlDescription = ({ href }: { href: string }) => { - return ( - - {KIBANA_QUERY_LANGUAGE} - - ), - searchField: Monitor Name, ID, Url, - }} - /> - ); -}; - -const KIBANA_QUERY_LANGUAGE = i18n.translate('xpack.uptime.query.queryBar.kqlFullLanguageName', { - defaultMessage: 'Kibana Query Language', -}); 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 deleted file mode 100644 index a41fa656ec63d..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useRef, useEffect, RefObject } from 'react'; -import { EuiSuggestItem } from '@elastic/eui'; - -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; - -const SuggestionItem = euiStyled.div<{ selected: boolean }>` - background: ${(props) => (props.selected ? props.theme.eui.euiColorLightestShade : 'initial')}; -`; - -function getIconColor(type: string) { - switch (type) { - case 'field': - return 'tint5'; - case 'value': - return 'tint0'; - case 'operator': - return 'tint1'; - case 'conjunction': - return 'tint3'; - case 'recentSearch': - return 'tint10'; - default: - return 'tint5'; - } -} - -function getEuiIconType(type: string) { - switch (type) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - throw new Error(`Unknown type ${type}`); - } -} - -interface SuggestionProps { - onClick: (sug: QuerySuggestion) => void; - onMouseEnter: () => void; - selected: boolean; - suggestion: QuerySuggestion; - innerRef: (node: any) => void; -} - -export const Suggestion: React.FC = ({ - innerRef, - selected, - suggestion, - onClick, - onMouseEnter, -}) => { - const childNode: RefObject = useRef(null); - - useEffect(() => { - if (childNode.current) { - innerRef(childNode.current); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [childNode]); - - return ( - - onClick(suggestion)} - onMouseEnter={onMouseEnter} - // @ts-ignore - description={suggestion.description} - /> - - ); -}; 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 deleted file mode 100644 index 9b382772346d1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useRef, useState, useEffect } from 'react'; -import { isEmpty } from 'lodash'; -import { rgba } from 'polished'; -import { Suggestion } from './suggestion'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; - -export const unit = 16; - -export const units = { - unit, - eighth: unit / 8, - quarter: unit / 4, - half: unit / 2, - minus: unit * 0.75, - plus: unit * 1.5, - double: unit * 2, - triple: unit * 3, - quadruple: unit * 4, -}; - -export function px(value: number): string { - return `${value}px`; -} - -const List = euiStyled.ul` - width: 100%; - border: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; - border-radius: ${px(units.quarter)}; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - z-index: 10; - max-height: ${px(unit * 20)}; - overflow: auto; - position: absolute; - - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } -`; - -interface SuggestionsProps { - index: number; - onClick: (sug: QuerySuggestion) => void; - onMouseEnter: (index: number) => void; - show?: boolean; - suggestions: QuerySuggestion[]; - loadMore: () => void; -} - -export const Suggestions: React.FC = ({ - show, - index, - onClick, - suggestions, - onMouseEnter, - loadMore, -}) => { - const [childNodes, setChildNodes] = useState([]); - - const parentNode = useRef(null); - - useEffect(() => { - const scrollIntoView = () => { - const parent = parentNode.current; - const child = childNodes[index]; - - if (index == null || !parent || !child) { - return; - } - - const scrollTop = Math.max( - Math.min(parent.scrollTop, child.offsetTop), - child.offsetTop + child.offsetHeight - parent.offsetHeight - ); - - parent.scrollTop = scrollTop; - }; - scrollIntoView(); - }, [index, childNodes]); - - if (!show || isEmpty(suggestions)) { - return null; - } - - const handleScroll = () => { - const parent = parentNode.current; - - if (!loadMore || !parent) { - return; - } - - const position = parent.scrollTop + parent.offsetHeight; - const height = parent.scrollHeight; - const remaining = height - position; - const margin = 50; - - if (!height || !position) { - return; - } - if (remaining <= margin) { - loadMore(); - } - }; - - const suggestionsNodes = suggestions.map((suggestion, currIndex) => { - const key = suggestion + '_' + currIndex; - return ( - { - const nodes = childNodes; - nodes[currIndex] = node; - setChildNodes([...nodes]); - }} - selected={currIndex === index} - suggestion={suggestion} - onClick={onClick} - onMouseEnter={() => onMouseEnter(currIndex)} - key={key} - /> - ); - }); - - return ( - - {suggestionsNodes} - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx deleted file mode 100644 index ed75747aa3416..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { Typeahead } from './typehead'; -import { render } from '../../../../lib/helper/rtl_helpers'; - -describe('Type head', () => { - jest.useFakeTimers(); - - it('it sets initial value', () => { - const { getByTestId, getByDisplayValue, history } = render( - {}} - suggestions={[]} - loadMore={() => {}} - queryExample="" - /> - ); - - const input = getByTestId('uptimeKueryBarInput'); - - expect(input).toBeInTheDocument(); - expect(getByDisplayValue('elastic')).toBeInTheDocument(); - - fireEvent.change(input, { target: { value: 'kibana' } }); - - // to check if it updateds the query params, needed for debounce wait - jest.advanceTimersByTime(250); - - expect(history.location.search).toBe('?query=kibana'); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx deleted file mode 100644 index e4dd175b2fe1b..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ChangeEvent, MouseEvent, useState, useRef, useEffect } from 'react'; -import { EuiFieldSearch, EuiProgress, EuiOutsideClickDetector } from '@elastic/eui'; -import { Suggestions } from './suggestions'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { SearchType } from './search_type/search_type'; -import { useKqlSyntax } from './use_kql_syntax'; -import { useKeyEvents } from './use_key_events'; -import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations'; -import { useSimpleQuery } from './use_simple_kuery'; - -interface TypeaheadProps { - onChange: (inputValue: string, selectionStart: number | null) => void; - onSubmit: (inputValue: string) => void; - suggestions: QuerySuggestion[]; - queryExample: string; - initialValue?: string; - isLoading?: boolean; - disabled?: boolean; - dataTestSubj: string; - ariaLabel: string; - loadMore: () => void; -} - -export const Typeahead: React.FC = ({ - initialValue, - suggestions, - onChange, - onSubmit, - dataTestSubj, - ariaLabel, - disabled, - isLoading, - loadMore, -}) => { - const [value, setValue] = useState(''); - const [index, setIndex] = useState(null); - const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(false); - - const [selected, setSelected] = useState(null); - const [inputIsPristine, setInputIsPristine] = useState(true); - const [lastSubmitted, setLastSubmitted] = useState(''); - - const { kqlSyntax, setKqlSyntax } = useKqlSyntax({ setValue }); - - const inputRef = useRef(); - - const { setQuery } = useSimpleQuery(); - - useEffect(() => { - if (inputIsPristine && initialValue) { - setValue(initialValue); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialValue]); - - const selectSuggestion = (suggestion: QuerySuggestion) => { - const nextInputValue = - value.substr(0, suggestion.start) + suggestion.text + value.substr(suggestion.end); - - setValue(nextInputValue); - setSelected(suggestion); - setIndex(null); - - onChange(nextInputValue, nextInputValue.length); - }; - - const { onKeyDown, onKeyUp } = useKeyEvents({ - index, - value, - isSuggestionsVisible, - setIndex, - setIsSuggestionsVisible, - suggestions, - selectSuggestion, - onChange, - onSubmit, - }); - - const onClickOutside = () => { - if (isSuggestionsVisible) { - setIsSuggestionsVisible(false); - onSuggestionSubmit(); - } - }; - - const onChangeInputValue = (event: ChangeEvent) => { - const { value: valueN, selectionStart } = event.target; - const hasValue = Boolean(valueN.trim()); - - setValue(valueN); - - setInputIsPristine(false); - setIndex(null); - - if (!kqlSyntax) { - setQuery(valueN); - return; - } - - setIsSuggestionsVisible(hasValue); - - if (!hasValue) { - onSubmit(valueN); - } - onChange(valueN, selectionStart!); - }; - - const onClickInput = (event: MouseEvent & ChangeEvent) => { - if (kqlSyntax) { - event.stopPropagation(); - const { selectionStart } = event.target; - onChange(value, selectionStart!); - } - }; - - const onFocus = () => { - if (kqlSyntax) { - setIsSuggestionsVisible(true); - } - }; - - const onClickSuggestion = (suggestion: QuerySuggestion) => { - selectSuggestion(suggestion); - if (inputRef.current) inputRef.current.focus(); - }; - - const onMouseEnterSuggestion = (indexN: number) => { - setIndex(indexN); - }; - - const onSuggestionSubmit = () => { - if ( - lastSubmitted !== value && - selected && - (selected.type === 'value' || selected.text.trim() === ': *') - ) { - onSubmit(value); - - setLastSubmitted(value); - setSelected(null); - } - }; - - return ( - - -
- { - if (node) { - inputRef.current = node; - } - }} - disabled={disabled} - value={value} - onKeyDown={kqlSyntax ? onKeyDown : undefined} - onKeyUp={kqlSyntax ? onKeyUp : undefined} - onFocus={onFocus} - onChange={onChangeInputValue} - onClick={onClickInput} - autoComplete="off" - spellCheck={false} - data-test-subj={'uptimeKueryBarInput'} - append={} - /> - - {isLoading && ( - - )} -
- {kqlSyntax && ( - - )} -
-
- ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts deleted file mode 100644 index ac702cc95dd64..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ChangeEvent, KeyboardEvent } from 'react'; -import * as React from 'react'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, -}; - -interface Props { - value: string; - index: number | null; - isSuggestionsVisible: boolean; - setIndex: React.Dispatch>; - setIsSuggestionsVisible: React.Dispatch>; - suggestions: QuerySuggestion[]; - selectSuggestion: (suggestion: QuerySuggestion) => void; - onChange: (inputValue: string, selectionStart: number | null) => void; - onSubmit: (inputValue: string) => void; -} - -export const useKeyEvents = ({ - value, - index, - isSuggestionsVisible, - setIndex, - setIsSuggestionsVisible, - suggestions, - selectSuggestion, - onChange, - onSubmit, -}: Props) => { - const incrementIndex = (currentIndex: number) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= suggestions.length) { - nextIndex = 0; - } - - setIndex(nextIndex); - }; - - const decrementIndex = (currentIndex: number) => { - let previousIndex: number | null = currentIndex - 1; - if (previousIndex < 0) { - previousIndex = null; - } - setIndex(previousIndex); - }; - - const onKeyUp = (event: KeyboardEvent & ChangeEvent) => { - const { selectionStart } = event.target; - switch (event.keyCode) { - case KEY_CODES.LEFT: - setIsSuggestionsVisible(true); - onChange(value, selectionStart); - break; - case KEY_CODES.RIGHT: - setIsSuggestionsVisible(true); - onChange(value, selectionStart); - break; - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible) { - incrementIndex(index!); - } else { - setIndex(0); - setIsSuggestionsVisible(true); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible) { - decrementIndex(index!); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && suggestions[index!]) { - selectSuggestion(suggestions[index!]); - } else { - setIsSuggestionsVisible(false); - onSubmit(value); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - setIsSuggestionsVisible(false); - break; - case KEY_CODES.TAB: - setIsSuggestionsVisible(false); - break; - } - }; - - return { onKeyUp, onKeyDown }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts deleted file mode 100644 index 2c945c33b9dc7..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { KQL_SYNTAX_LOCAL_STORAGE } from '../../../../../common/constants'; -import { useUrlParams } from '../../../../hooks'; - -interface Props { - setValue: React.Dispatch>; -} - -export const useKqlSyntax = ({ setValue }: Props) => { - const [kqlSyntax, setKqlSyntax] = useState( - localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true' - ); - - const [getUrlParams] = useUrlParams(); - - const { query, search } = getUrlParams(); - - useEffect(() => { - setValue(query || ''); - }, [query, setValue]); - - useEffect(() => { - setValue(search || ''); - }, [search, setValue]); - - useEffect(() => { - if (query || search) { - // if url has query or params we will give them preference on load - // for selecting syntax type - if (query) { - setKqlSyntax(false); - } - if (search) { - setKqlSyntax(true); - } - } else { - setKqlSyntax(localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true'); - } - // This part is meant to run only when component loads - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - localStorage.setItem(KQL_SYNTAX_LOCAL_STORAGE, String(kqlSyntax)); - setValue(''); - }, [kqlSyntax, setValue]); - - return { kqlSyntax, setKqlSyntax }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts deleted file mode 100644 index 55df62a7e14d6..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; -import { useUrlParams } from '../../../../hooks'; - -export const useSimpleQuery = () => { - const [getUrlParams, updateUrlParams] = useUrlParams(); - - const { query } = getUrlParams(); - - const [debouncedValue, setDebouncedValue] = useState(query ?? ''); - - useEffect(() => { - setDebouncedValue(query ?? ''); - }, [query]); - - useDebounce( - () => { - updateUrlParams({ query: debouncedValue }); - }, - 250, - [debouncedValue] - ); - - return { query, setQuery: setDebouncedValue }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index 5ac6351ba5dec..4fd0a9c0f4b08 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -8,7 +8,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { getMonitorList } from '../../../state/actions'; -import { monitorListSelector } from '../../../state/selectors'; +import { esKuerySelector, monitorListSelector } from '../../../state/selectors'; import { MonitorListComponent } from './monitor_list'; import { useUrlParams } from '../../../hooks'; import { UptimeRefreshContext } from '../../../contexts'; @@ -28,7 +28,7 @@ const getPageSizeValue = () => { }; export const MonitorList: React.FC = (props) => { - const { filters } = props; + const filters = useSelector(esKuerySelector); const [pageSize, setPageSize] = useState(getPageSizeValue); diff --git a/x-pack/plugins/uptime/public/components/overview/overview_container.tsx b/x-pack/plugins/uptime/public/components/overview/overview_container.tsx deleted file mode 100644 index bd5ccafc5b854..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/overview_container.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useDispatch, useSelector } from 'react-redux'; -import React, { useCallback } from 'react'; -import { OverviewPageComponent } from '../../pages/overview'; -import { selectIndexPattern } from '../../state/selectors'; -import { setEsKueryString } from '../../state/actions'; - -export const OverviewPage: React.FC = (props) => { - const dispatch = useDispatch(); - - const setEsKueryFilters = useCallback( - (esFilters: string) => dispatch(setEsKueryString(esFilters)), - [dispatch] - ); - const { index_pattern: indexPattern, loading } = useSelector(selectIndexPattern); - - return ( - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.test.tsx b/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.test.tsx deleted file mode 100644 index 782a60096be5b..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { ParsingErrorCallout } from './parsing_error_callout'; - -describe('OverviewPageParsingErrorCallout', () => { - it('renders without errors when a valid error is provided', () => { - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); - - it('renders without errors when an error with no message is provided', () => { - const error: any = {}; - expect(shallowWithIntl()).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx b/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx deleted file mode 100644 index 07abe02e5af1d..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -interface HasMessage { - message: string; -} - -interface ParsingErrorCalloutProps { - error: HasMessage; -} - -export const ParsingErrorCallout = ({ error }: ParsingErrorCalloutProps) => ( - -

- - {error.message - ? error.message - : i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.noMessage', { - defaultMessage: 'There was no error message', - })} - - ), - }} - /> -

-
-); diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx new file mode 100644 index 0000000000000..39bad0663ff38 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexItem } from '@elastic/eui'; +import { QueryStringInput } from '../../../../../../../src/plugins/data/public/'; +import { useIndexPattern } from './use_index_pattern'; +import { SyntaxType, useQueryBar } from './use_query_bar'; +import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations'; +import { useGetUrlParams } from '../../../hooks'; + +const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; + +export const isValidKuery = (query: string) => { + if (query === '') { + return true; + } + const listOfOperators = [':', '>=', '=>', '>', '<']; + for (let i = 0; i < listOfOperators.length; i++) { + const operator = listOfOperators[i]; + const qParts = query.trim().split(operator); + if (query.includes(operator) && qParts.length > 1 && qParts[1]) { + return true; + } + } + return false; +}; + +export const QueryBar = () => { + const { index_pattern: indexPattern } = useIndexPattern(); + + const { search: urlValue } = useGetUrlParams(); + + const { query, setQuery } = useQueryBar(); + + const [inputVal, setInputVal] = useState(query.query); + + const isInValid = () => { + if (query.language === SyntaxType.text) { + return false; + } + return inputVal?.trim() !== urlValue?.trim(); + }; + + return ( + + { + if (queryN?.language === SyntaxType.text) { + setQuery({ query: queryN.query as string, language: queryN.language }); + } + if (queryN?.language === SyntaxType.kuery && isValidKuery(queryN?.query as string)) { + // we want to submit when user clears or paste a complete kuery + setQuery({ query: queryN.query as string, language: queryN.language }); + } + setInputVal(queryN?.query as string); + }} + onSubmit={(queryN) => { + if (queryN) setQuery({ query: queryN.query as string, language: queryN.language }); + }} + query={{ ...query, query: inputVal }} + aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', { + defaultMessage: 'Input filter criteria for the overview page', + })} + data-test-subj="uptimeSearchBarInput" + autoSubmit={true} + storageKey={SYNTAX_STORAGE} + placeholder={ + query.language === SyntaxType.kuery ? KQL_PLACE_HOLDER : SIMPLE_SEARCH_PLACEHOLDER + } + isInvalid={isInValid()} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/translations.ts similarity index 56% rename from x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts rename to x-pack/plugins/uptime/public/components/overview/query_bar/translations.ts index e0d36bcb57587..6bb4ebbf098a9 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/translations.ts @@ -18,21 +18,3 @@ export const SIMPLE_SEARCH_PLACEHOLDER = i18n.translate( defaultMessage: 'Search by monitor ID, name, or url (E.g. http:// )', } ); - -export const CHANGE_SEARCH_BAR_SYNTAX = i18n.translate( - 'xpack.uptime.kueryBar.options.syntax.changeLabel', - { - defaultMessage: 'Change search bar syntax to use Kibana Query Language', - } -); - -export const CHANGE_SEARCH_BAR_SYNTAX_SIMPLE = i18n.translate( - 'xpack.uptime.kueryBar.options.syntax.simple', - { - defaultMessage: 'Change search bar syntax to not use Kibana Query Language', - } -); - -export const SYNTAX_OPTIONS_LABEL = i18n.translate('xpack.uptime.kueryBar.options.syntax', { - defaultMessage: 'SYNTAX OPTIONS', -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/use_index_pattern.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/use_index_pattern.ts similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/kuery_bar/use_index_pattern.ts rename to x-pack/plugins/uptime/public/components/overview/query_bar/use_index_pattern.ts diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts new file mode 100644 index 0000000000000..caf6b08e8fdea --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useDebounce } from 'react-use'; +import { useDispatch } from 'react-redux'; +import { useGetUrlParams, useUpdateKueryString, useUrlParams } from '../../../hooks'; +import { setEsKueryString } from '../../../state/actions'; +import { useIndexPattern } from './use_index_pattern'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { UptimePluginServices } from '../../../apps/plugin'; + +export enum SyntaxType { + text = 'text', + kuery = 'kuery', +} +const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; + +export const useQueryBar = () => { + const { index_pattern: indexPattern } = useIndexPattern(); + + const dispatch = useDispatch(); + + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); + const { search, query: queryParam, filters: paramFilters } = params; + + const { + services: { storage }, + } = useKibana(); + + const [query, setQuery] = useState( + queryParam + ? { + query: queryParam, + language: SyntaxType.text, + } + : search + ? { query: search, language: SyntaxType.kuery } + : { + query: '', + language: storage.get(SYNTAX_STORAGE) ?? SyntaxType.text, + } + ); + + const updateUrlParams = useUrlParams()[1]; + + const [esFilters, error] = useUpdateKueryString( + indexPattern, + query.language === SyntaxType.kuery ? (query.query as string) : undefined, + paramFilters + ); + + const setEsKueryFilters = useCallback( + (esFiltersN: string) => dispatch(setEsKueryString(esFiltersN)), + [dispatch] + ); + + useEffect(() => { + setEsKueryFilters(esFilters ?? ''); + }, [esFilters, setEsKueryFilters]); + + useDebounce( + () => { + if (query.language === SyntaxType.text && queryParam !== query.query) { + updateUrlParams({ query: query.query as string }); + } + if (query.language === SyntaxType.kuery) { + updateUrlParams({ query: '' }); + } + }, + 350, + [query] + ); + + useDebounce( + () => { + if (query.language === SyntaxType.kuery && !error && esFilters) { + updateUrlParams({ search: query.query as string }); + } + if (query.language === SyntaxType.text) { + updateUrlParams({ search: '' }); + } + if (query.language === SyntaxType.kuery && query.query === '') { + updateUrlParams({ search: '' }); + } + }, + 250, + [esFilters, error] + ); + + return { query, setQuery }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/__snapshots__/snapshot.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/snapshot/__snapshots__/snapshot.test.tsx.snap similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/__snapshots__/snapshot.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/snapshot/__snapshots__/snapshot.test.tsx.snap diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts b/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts index bd8fe22620ee9..a61d8e07bb2d6 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts @@ -6,4 +6,3 @@ */ export { SnapshotComponent } from './snapshot'; -export { Snapshot } from './snapshot_container'; diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot.test.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.test.tsx similarity index 62% rename from x-pack/plugins/uptime/public/components/overview/snapshot.test.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.test.tsx index a39fabeef983a..184f6b73cc7ab 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.test.tsx @@ -7,8 +7,9 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; -import { Snapshot } from '../../../common/runtime_types'; -import { SnapshotComponent } from './snapshot/snapshot'; +import { SnapshotComponent } from './snapshot'; +import { Snapshot } from '../../../../common/runtime_types/snapshot'; +import * as hook from './use_snap_shot'; describe('Snapshot component', () => { const snapshot: Snapshot = { @@ -18,7 +19,9 @@ describe('Snapshot component', () => { }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + jest.spyOn(hook, 'useSnapShotCount').mockReturnValue({ count: snapshot, loading: false }); + + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx index d02ef79d42d06..53ccc0b8c3bf1 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx @@ -10,13 +10,11 @@ import React from 'react'; import { DonutChart } from '../../common/charts'; import { ChartWrapper } from '../../common/charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; +import { useSnapShotCount } from './use_snap_shot'; const SNAPSHOT_CHART_HEIGHT = 144; interface SnapshotComponentProps { - count: SnapshotType; - loading: boolean; height?: string; } @@ -25,10 +23,14 @@ interface SnapshotComponentProps { * glean the status of their uptime environment. * @param props the props required by the component */ -export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( - - - - - -); +export const SnapshotComponent: React.FC = ({ height }) => { + const { count, loading } = useSnapShotCount(); + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx deleted file mode 100644 index 09c832c603d10..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useGetUrlParams } from '../../../hooks'; -import { getSnapshotCountAction } from '../../../state/actions'; -import { SnapshotComponent } from './snapshot'; -import { esKuerySelector, snapshotDataSelector } from '../../../state/selectors'; -import { UptimeRefreshContext } from '../../../contexts'; - -interface Props { - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; -} - -export const Snapshot: React.FC = ({ height }: Props) => { - const { dateRangeStart, dateRangeEnd, query } = useGetUrlParams(); - - const { lastRefresh } = useContext(UptimeRefreshContext); - - const { count, loading } = useSelector(snapshotDataSelector); - const esKuery = useSelector(esKuerySelector); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(getSnapshotCountAction.get({ query, dateRangeStart, dateRangeEnd, filters: esKuery })); - }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, dispatch, query]); - return ; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/use_snap_shot.ts b/x-pack/plugins/uptime/public/components/overview/snapshot/use_snap_shot.ts new file mode 100644 index 0000000000000..2d1a9691d60d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/use_snap_shot.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { useGetUrlParams } from '../../../hooks'; +import { esKuerySelector } from '../../../state/selectors'; +import { UptimeRefreshContext } from '../../../contexts'; +import { useFetcher } from '../../../../../observability/public'; +import { fetchSnapshotCount } from '../../../state/api'; + +export const useSnapShotCount = () => { + const { dateRangeStart, dateRangeEnd, query } = useGetUrlParams(); + + const { lastRefresh } = useContext(UptimeRefreshContext); + + const esKuery = useSelector(esKuerySelector); + + const { data, loading } = useFetcher( + () => fetchSnapshotCount({ query, dateRangeStart, dateRangeEnd, filters: esKuery }), + [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, query] + ); + + return { count: data || { total: 0, up: 0, down: 0 }, loading }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/status_panel.tsx b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx index 10bf20aa5f5e4..6faa56bb358fb 100644 --- a/x-pack/plugins/uptime/public/components/overview/status_panel.tsx +++ b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { PingHistogram } from '../monitor'; -import { Snapshot } from './snapshot/snapshot_container'; +import { SnapshotComponent } from './snapshot'; const STATUS_CHART_HEIGHT = '160px'; @@ -16,7 +16,7 @@ export const StatusPanel = ({}) => ( - + diff --git a/x-pack/plugins/uptime/public/hooks/__snapshots__/use_url_params.test.tsx.snap b/x-pack/plugins/uptime/public/hooks/__snapshots__/use_url_params.test.tsx.snap index d35befe674071..0b8893e36a366 100644 --- a/x-pack/plugins/uptime/public/hooks/__snapshots__/use_url_params.test.tsx.snap +++ b/x-pack/plugins/uptime/public/hooks/__snapshots__/use_url_params.test.tsx.snap @@ -209,7 +209,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = ` } >
- {"pagination":"foo","absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false} + {"pagination":"foo","absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false,"query":""}
+ + ); + }; + + const customTestbed = registerTestBed(TestComponent, { + memoryRouter: { + wrapComponent: false, + }, + })() as TestBed; + + testBed = { + ...customTestbed, + actions: getCommonActions(customTestbed), + }; + + const { + form, + component, + find, + actions: { changeFieldType }, + } = testBed; + + // We set some dummy painless error + act(() => { + find('setPainlessErrorButton').simulate('click'); + }); + component.update(); + + expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); + + // We change the type and expect the form error to not be there anymore + await changeFieldType('long'); + expect(form.getErrorsMessages()).toEqual([]); + }); }); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index afb87bd1e7334..3785096e20627 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -21,6 +21,7 @@ import type { CoreStart } from 'src/core/public'; import { Form, useForm, + useFormData, FormHook, UseField, TextField, @@ -184,6 +185,9 @@ const FieldEditorComponent = ({ serializer: formSerializer, }); const { submit, isValid: isFormValid, isSubmitted } = form; + const { clear: clearSyntaxError } = syntaxError; + + const [{ type }] = useFormData({ form }); const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); @@ -194,6 +198,12 @@ const FieldEditorComponent = ({ } }, [onChange, isFormValid, isSubmitted, submit]); + useEffect(() => { + // Whenever the field "type" changes we clear any possible painless syntax + // error as it is possibly stale. + clearSyntaxError(); + }, [type, clearSyntaxError]); + return (
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts index 46414c264c6b7..286931ad0e854 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -140,7 +140,7 @@ describe('', () => { find, component, form, - actions: { toggleFormRow }, + actions: { toggleFormRow, changeFieldType }, } = setup({ ...defaultProps, onSave }); act(() => { @@ -173,14 +173,7 @@ describe('', () => { }); // Change the type and make sure it is forwarded - act(() => { - find('typeField').simulate('change', [ - { - label: 'Other type', - value: 'other_type', - }, - ]); - }); + await changeFieldType('other_type', 'Other type'); await act(async () => { find('fieldSaveButton').simulate('click'); diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts index 295c32cf28e78..b55a59df34545 100644 --- a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts +++ b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { act } from 'react-dom/test-utils'; import { TestBed } from './test_utils'; export const getCommonActions = (testBed: TestBed) => { @@ -21,7 +22,20 @@ export const getCommonActions = (testBed: TestBed) => { testBed.form.toggleEuiSwitch(testSubj); }; + const changeFieldType = async (value: string, label?: string) => { + await act(async () => { + testBed.find('typeField').simulate('change', [ + { + value, + label: label ?? value, + }, + ]); + }); + testBed.component.update(); + }; + return { toggleFormRow, + changeFieldType, }; }; From 2efea063926c5a1939df965eb110d9d30fa0ec2f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 7 Apr 2021 14:52:08 +0300 Subject: [PATCH 055/131] [TSVB] [Regression] Fix Top Hit / Filter Ratio aggregations (#96288) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/aggs/calculation.js | 2 +- .../public/application/components/aggs/cumulative_sum.js | 2 +- .../public/application/components/aggs/derivative.js | 2 +- .../public/application/components/aggs/filter_ratio.js | 3 ++- .../public/application/components/aggs/math.js | 2 +- .../public/application/components/aggs/moving_average.js | 2 +- .../public/application/components/aggs/positive_only.js | 2 +- .../public/application/components/aggs/serial_diff.js | 2 +- .../public/application/components/aggs/std_sibling.js | 2 +- .../public/application/components/aggs/top_hit.js | 5 +++-- .../public/application/components/aggs/vars.js | 2 +- .../public/application/components/splits/terms.js | 2 +- 12 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index 42321c2728198..7a29db27a514f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -131,7 +131,7 @@ export function CalculationAgg(props) { CalculationAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index 8a597ffa9d5e8..d82bcbcd885cb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -84,7 +84,7 @@ export function CumulativeSumAgg(props) { CumulativeSumAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index 8d155b378755a..6f7e5680b2a86 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -110,7 +110,7 @@ export const DerivativeAgg = (props) => { DerivativeAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 7f93567980b2d..90353f9af8e35 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -13,6 +13,7 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import { htmlIdGenerator, @@ -29,7 +30,7 @@ import { getDataStart } from '../../../services'; import { QueryBarWrapper } from '../query_bar_wrapper'; const isFieldHistogram = (fields, indexPattern, field) => { - const indexFields = fields[indexPattern]; + const indexFields = fields[getIndexPatternKey(indexPattern)]; if (!indexFields) return false; const fieldObject = indexFields.find((f) => f.name === field); if (!fieldObject) return false; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index 5fa9912ae17e7..e92659e677860 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -150,7 +150,7 @@ export function MathAgg(props) { MathAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index bf6c95202ed25..3c53e4597136e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -305,7 +305,7 @@ export const MovingAverageAgg = (props) => { MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 55c14e61bed1a..010a88146595b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -88,7 +88,7 @@ export const PositiveOnlyAgg = (props) => { PositiveOnlyAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index 00688992f819b..675a9868e13b3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -115,7 +115,7 @@ export const SerialDiffAgg = (props) => { SerialDiffAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index d3ff4f64b5351..bebc1cf2bce72 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -163,7 +163,7 @@ const StandardSiblingAggUi = (props) => { StandardSiblingAggUi.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 92e754c1dcdaf..12f7ad143cb25 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -26,6 +26,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; import { PANEL_TYPES } from '../../../../common/panel_types'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const isFieldTypeEnabled = (fieldRestrictions, fieldType) => fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; @@ -115,8 +116,8 @@ const TopHitAggUi = (props) => { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - - const field = fields[indexPattern].find((f) => f.name === model.field); + const fieldsSelector = getIndexPatternKey(indexPattern); + const field = fields[fieldsSelector].find((f) => f.name === model.field); const aggWithOptions = getAggWithOptions(field, aggWithOptionsRestrictFields); const orderOptions = getOrderOptions(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index ca310ab4153d1..b9d554e254bcc 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -96,7 +96,7 @@ CalculationVars.defaultProps = { CalculationVars.propTypes = { fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), metrics: PropTypes.array, model: PropTypes.object, name: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index b996abd6373ab..ab5342e925bd7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -237,7 +237,7 @@ SplitByTermsUI.propTypes = { intl: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), fields: PropTypes.object, uiRestrictions: PropTypes.object, seriesQuantity: PropTypes.object, From 7584b728c647dffbbbdb7c39545369592ddfda38 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 7 Apr 2021 15:36:55 +0300 Subject: [PATCH 056/131] [Search Sessions] Monitoring hardening part 1 (#96196) * Decrease default pageSize to 100 Set default strategy Don't create sessions when disabled Clear monitoring task when disabled Use concatMap to serialize session checkup * ts * ts * ts * Update x-pack/plugins/data_enhanced/server/search/session/session_service.ts Co-authored-by: Lukas Olson * Search sessions are disabled * Clear task on server start Co-authored-by: Lukas Olson --- x-pack/plugins/data_enhanced/config.ts | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 29 +- .../search/session/check_running_sessions.ts | 4 +- .../server/search/session/monitoring_task.ts | 29 +- .../search/session/session_service.test.ts | 1685 +++++++++-------- .../server/search/session/session_service.ts | 25 +- x-pack/plugins/data_enhanced/server/type.ts | 19 + 7 files changed, 959 insertions(+), 834 deletions(-) diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index fc1f22d50b09f..8cbf930fe87bd 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -18,7 +18,7 @@ export const configSchema = schema.object({ * pageSize controls how many search session objects we load at once while monitoring * session completion */ - pageSize: schema.number({ defaultValue: 10000 }), + pageSize: schema.number({ defaultValue: 100 }), /** * trackingInterval controls how often we track search session objects progress */ diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 462d1fc337ae2..ae36b881796c4 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,13 +6,7 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, - usageProvider, -} from '../../../../src/plugins/data/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { usageProvider } from '../../../../src/plugins/data/server'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; import { registerSessionRoutes } from './routes'; import { searchSessionSavedObjectType } from './saved_objects'; @@ -22,22 +16,13 @@ import { eqlSearchStrategyProvider, } from './search'; import { getUiSettings } from './ui_settings'; -import type { DataEnhancedRequestHandlerContext } from './type'; +import type { + DataEnhancedRequestHandlerContext, + DataEnhancedSetupDependencies as SetupDependencies, + DataEnhancedStartDependencies as StartDependencies, +} from './type'; import { ConfigSchema } from '../config'; import { registerUsageCollector } from './collectors'; -import { SecurityPluginSetup } from '../../security/server'; - -interface SetupDependencies { - data: DataPluginSetup; - usageCollection?: UsageCollectionSetup; - taskManager: TaskManagerSetupContract; - security?: SecurityPluginSetup; -} - -export interface StartDependencies { - data: DataPluginStart; - taskManager: TaskManagerStartContract; -} export class EnhancedDataServerPlugin implements Plugin { @@ -50,7 +35,7 @@ export class EnhancedDataServerPlugin this.config = this.initializerContext.config.get(); } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { const usage = deps.usageCollection ? usageProvider(core) : undefined; core.uiSettings.register(getUiSettings()); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index 6e52b17f36803..60c7283320d0c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; -import { expand, mergeMap } from 'rxjs/operators'; +import { expand, concatMap } from 'rxjs/operators'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { ENHANCED_ES_SEARCH_STRATEGY, @@ -154,7 +154,7 @@ export async function checkRunningSessions( try { await getAllSavedSearchSessions$(deps, config) .pipe( - mergeMap(async (runningSearchSessionsResponse) => { + concatMap(async (runningSearchSessionsResponse) => { if (!runningSearchSessionsResponse.total) return; logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 8aa35def387b7..101ccb14edf67 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -15,6 +15,7 @@ import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; import { ConfigSchema } from '../../../config'; import { SEARCH_SESSION_TYPE } from '../../../common'; +import { DataEnhancedStartDependencies } from '../../type'; export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; @@ -25,12 +26,19 @@ interface SearchSessionTaskDeps { config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { +function searchSessionRunner( + core: CoreSetup, + { logger, config }: SearchSessionTaskDeps +) { return ({ taskInstance }: RunContext) => { return { async run() { const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug('Search sessions are disabled. Skipping task.'); + return; + } const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); await checkRunningSessions( @@ -50,7 +58,10 @@ function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionT }; } -export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionTaskDeps) { +export function registerSearchSessionsTask( + core: CoreSetup, + deps: SearchSessionTaskDeps +) { deps.taskManager.registerTaskDefinitions({ [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', @@ -59,6 +70,18 @@ export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionT }); } +export async function unscheduleSearchSessionsTask( + taskManager: TaskManagerStartContract, + logger: Logger +) { + try { + await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); + logger.debug(`Search sessions cleared`); + } catch (e) { + logger.error(`Error clearing task, received ${e.message}`); + } +} + export async function scheduleSearchSessionsTasks( taskManager: TaskManagerStartContract, logger: Logger, @@ -79,6 +102,6 @@ export async function scheduleSearchSessionsTasks( logger.debug(`Search sessions task, scheduled to run`); } catch (e) { - logger.debug(`Error scheduling task, received ${e.message}`); + logger.error(`Error scheduling task, received ${e.message}`); } } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index f61d89e2301ab..9344ab973c636 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -15,12 +15,12 @@ import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../common'; import { SearchSessionService } from './session_service'; import { createRequestHash } from './utils'; import moment from 'moment'; -import { coreMock } from 'src/core/server/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; import { ConfigSchema } from '../../../config'; -// @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { AuthenticatedUser } from '../../../../security/common/model'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { TaskManagerStartContract } from '../../../../task_manager/server'; const MAX_UPDATE_RETRIES = 3; @@ -29,6 +29,7 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; let service: SearchSessionService; + let mockTaskManager: jest.Mocked; const MOCK_STRATEGY = 'ese'; @@ -62,925 +63,1009 @@ describe('SearchSessionService', () => { references: [], }; - beforeEach(async () => { - savedObjectsClient = savedObjectsClientMock.create(); - const config: ConfigSchema = { - search: { - sessions: { - enabled: true, - pageSize: 10000, - notTouchedInProgressTimeout: moment.duration(1, 'm'), - notTouchedTimeout: moment.duration(2, 'm'), - maxUpdateRetries: MAX_UPDATE_RETRIES, - defaultExpiration: moment.duration(7, 'd'), - trackingInterval: moment.duration(10, 's'), - management: {} as any, + describe('Feature disabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: false, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, + }, }, - }, - }; - const mockLogger: any = { - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - service = new SearchSessionService(mockLogger, config); - const coreStart = coreMock.createStart(); - const mockTaskManager = taskManagerMock.createStart(); - await flushPromises(); - await service.start(coreStart, { - taskManager: mockTaskManager, - }); - }); - - afterEach(() => { - service.stop(); - }); - - describe('save', () => { - it('throws if `name` is not provided', () => { - expect(() => - service.save({ savedObjectsClient }, mockUser1, sessionId, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('throws if `appId` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) - ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + afterEach(() => { + service.stop(); }); - it('throws if `generator id` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - }) - ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + it('task is cleared, if exists', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); }); - it('saving updates an existing saved object and persists it', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', + it('trackId ignores', async () => { + await service.trackId({ savedObjectsClient }, mockUser1, { params: {} }, '123', { + sessionId: '321', + strategy: MOCK_STRATEGY, }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); expect(savedObjectsClient.create).not.toHaveBeenCalled(); - - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).not.toHaveProperty('idMapping'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); }); - it('saving creates a new persisted saved object, if it did not exist', async () => { - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', - }); - - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options?.id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', {}); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); - expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); - expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); - expect(callAttributes).toHaveProperty('username', mockUser1.username); + it('Save throws', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toBeInstanceOf(Error); }); - it('throws error if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.get({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + it('Update throws', () => { + const attributes = { name: 'new_name' }; + const response = service.update({ savedObjectsClient }, mockUser1, sessionId, attributes); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - - await service.save( - { savedObjectsClient }, - - null, - sessionId, - { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - } - ); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - const [[, attributes]] = savedObjectsClient.create.mock.calls; - expect(attributes).toHaveProperty('realmType', undefined); - expect(attributes).toHaveProperty('realmName', undefined); - expect(attributes).toHaveProperty('username', undefined); + it('Cancel throws', () => { + const response = service.cancel({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); - }); - - describe('get', () => { - it('calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('getId throws', () => { + const response = service.getId({ savedObjectsClient }, mockUser1, {}, {}); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get({ savedObjectsClient }, null, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('Delete throws', () => { + const response = service.delete({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); }); - describe('find', () => { - it('calls saved objects client with user filter', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, mockUser1, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", + describe('Feature enabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: true, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, }, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); - }); - - it('mixes in passed-in filter as string and KQL node', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, + }, }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options1 = { filter: 'foobar' }; - const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); - - const options2 = { filter: nodeBuilder.is('foo', 'bar') }; - const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); - - expect(response1).toBe(mockResponse); - expect(response2).toBe(mockResponse); - - const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; - expect(findOptions1).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": null, - }, - Object { - "type": "literal", - "value": "foobar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); - expect(findOptions2).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "foo", - }, - Object { - "type": "literal", - "value": "bar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('has no filter without security', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, null, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": undefined, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); + afterEach(() => { + service.stop(); }); - }); - - describe('update', () => { - it('update calls saved objects client with added touch time', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - const response = await service.update( - { savedObjectsClient }, - mockUser1, - sessionId, - attributes - ); + it('task is cleared and re-created', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); + expect(mockTaskManager.ensureScheduled).toHaveBeenCalled(); + }); - expect(response).toBe(mockUpdateSavedObject); + describe('save', () => { + it('throws if `name` is not provided', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + it('throws if `appId` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) + ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', attributes.name); - expect(callAttributes).toHaveProperty('touched'); - }); + it('throws if `generator id` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + }) + ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + }); - it('throws if user conflicts', () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('saving updates an existing saved object and persists it', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - expect( - service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - it('works without security', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).not.toHaveProperty('idMapping'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + }); - const attributes = { name: 'new_name' }; - const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - - expect(response).toBe(mockUpdateSavedObject); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', 'new_name'); - expect(callAttributes).toHaveProperty('touched'); - }); - }); + it('saving creates a new persisted saved object, if it did not exist', async () => { + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - describe('cancel', () => { - it('updates object status', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - await service.cancel({ savedObjectsClient }, mockUser1, sessionId); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); - }); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options?.id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', {}); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(callAttributes).toHaveProperty('username', mockUser1.username); + }); - it('throws if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - expect( - service.cancel({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + expect( + service.get({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('works without security', async () => { + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); - await service.cancel({ savedObjectsClient }, null, sessionId); + await service.save( + { savedObjectsClient }, - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + null, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + } + ); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', undefined); + expect(attributes).toHaveProperty('realmName', undefined); + expect(attributes).toHaveProperty('username', undefined); + }); }); - }); - describe('trackId', () => { - it('updates the saved object if search session already exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('get', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + const response = await service.get({ savedObjectsClient }, null, sessionId); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(callAttributes).toHaveProperty('touched'); }); - it('retries updating the saved object if there was a ES conflict 409', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - let counter = 0; - - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - } else { - resolve(mockUpdateSavedObject); + describe('find', () => { + it('calls saved objects client with user filter', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, mockUser1, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "page": 0, + "perPage": 5, + "type": "search-session", } - }); + `); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('mixes in passed-in filter as string and KQL node', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options1 = { filter: 'foobar' }; + const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); + + const options2 = { filter: nodeBuilder.is('foo', 'bar') }; + const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); + + expect(response1).toBe(mockResponse); + expect(response2).toBe(mockResponse); + + const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; + expect(findOptions1).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": null, + }, + Object { + "type": "literal", + "value": "foobar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + expect(findOptions2).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "foo", + }, + Object { + "type": "literal", + "value": "bar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('has no filter without security', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, null, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": undefined, + "page": 0, + "perPage": 5, + "type": "search-session", + } + `); + }); }); - it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('update', () => { + it('update calls saved objects client with added touch time', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update( + { savedObjectsClient }, + mockUser1, + sessionId, + attributes + ); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - }); + expect(response).toBe(mockUpdateSavedObject); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', attributes.name); + expect(callAttributes).toHaveProperty('touched'); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - // Track ID doesn't throw errors even in cases of failure! - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(response).toBe(mockUpdateSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', 'new_name'); + expect(callAttributes).toHaveProperty('touched'); + }); }); - it('creates the saved object in non persisted state, if search session doesnt exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('cancel', () => { + it('updates object status', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + await service.cancel({ savedObjectsClient }, mockUser1, sessionId); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.cancel({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options).toStrictEqual({ id: sessionId }); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + await service.cancel({ savedObjectsClient }, null, sessionId); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); }); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('sessionId', sessionId); - expect(callAttributes).toHaveProperty('persisted', false); }); - it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('trackId', () => { + it('updates the saved object if search session already exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - let counter = 0; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); - } else { - resolve(mockUpdateSavedObject); - } + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, }); + expect(callAttributes).toHaveProperty('touched'); }); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + it('retries updating the saved object if there was a ES conflict 409', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - }); + it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('retries everything at most MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + }); + }); - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + // Track ID doesn't throw errors even in cases of failure! + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - }); + it('creates the saved object in non persisted state, if search session doesnt exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('batches updates for the same session', async () => { - const sessionId1 = 'sessiondId1'; - const sessionId2 = 'sessiondId2'; + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - const searchRequest1 = { params: { 1: '1' } }; - const requestHash1 = createRequestHash(searchRequest1.params); - const searchId1 = 'searchId1'; + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - const searchRequest2 = { params: { 2: '2' } }; - const requestHash2 = createRequestHash(searchRequest2.params); - const searchId2 = 'searchId1'; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - const searchRequest3 = { params: { 3: '3' } }; - const requestHash3 = createRequestHash(searchRequest3.params); - const searchId3 = 'searchId3'; + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options).toStrictEqual({ id: sessionId }); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('sessionId', sessionId); + expect(callAttributes).toHaveProperty('persisted', false); + }); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await Promise.all([ - service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { - sessionId: sessionId2, + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }), - ]); + }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + }); - const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; - expect(type1).toBe(SEARCH_SESSION_TYPE); - expect(id1).toBe(sessionId1); - expect(callAttributes1).toHaveProperty('idMapping', { - [requestHash1]: { - id: searchId1, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, - [requestHash2]: { - id: searchId2, - status: SearchSessionStatus.IN_PROGRESS, + it('retries everything at most MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); }); - expect(callAttributes1).toHaveProperty('touched'); - const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; - expect(type2).toBe(SEARCH_SESSION_TYPE); - expect(id2).toBe(sessionId2); - expect(callAttributes2).toHaveProperty('idMapping', { - [requestHash3]: { - id: searchId3, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + it('batches updates for the same session', async () => { + const sessionId1 = 'sessiondId1'; + const sessionId2 = 'sessiondId2'; + + const searchRequest1 = { params: { 1: '1' } }; + const requestHash1 = createRequestHash(searchRequest1.params); + const searchId1 = 'searchId1'; + + const searchRequest2 = { params: { 2: '2' } }; + const requestHash2 = createRequestHash(searchRequest2.params); + const searchId2 = 'searchId1'; + + const searchRequest3 = { params: { 3: '3' } }; + const requestHash3 = createRequestHash(searchRequest3.params); + const searchId3 = 'searchId3'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + await Promise.all([ + service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { + sessionId: sessionId2, + strategy: MOCK_STRATEGY, + }), + ]); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; + expect(type1).toBe(SEARCH_SESSION_TYPE); + expect(id1).toBe(sessionId1); + expect(callAttributes1).toHaveProperty('idMapping', { + [requestHash1]: { + id: searchId1, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + [requestHash2]: { + id: searchId2, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes1).toHaveProperty('touched'); + + const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; + expect(type2).toBe(SEARCH_SESSION_TYPE); + expect(id2).toBe(sessionId2); + expect(callAttributes2).toHaveProperty('idMapping', { + [requestHash3]: { + id: searchId3, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes2).toHaveProperty('touched'); }); - expect(callAttributes2).toHaveProperty('touched'); }); - }); - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: true, + isRestore: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + [requestHash]: { + id: searchId, + }, + }, + }, + }; + savedObjectsClient.get.mockResolvedValue(mockSession); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { sessionId, isStored: true, - isRestore: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - [requestHash]: { - id: searchId, - }, - }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + isRestore: true, + }); - const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: true, - isRestore: true, + expect(id).toBe(searchId); }); - - expect(id).toBe(searchId); }); - }); - describe('getSearchIdMapping', () => { - it('retrieves the search IDs and strategies from the saved object', async () => { - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - foo: { - id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', - strategy: MOCK_STRATEGY, + describe('getSearchIdMapping', () => { + it('retrieves the search IDs and strategies from the saved object', async () => { + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + foo: { + id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', + strategy: MOCK_STRATEGY, + }, }, }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); - const searchIdMapping = await service.getSearchIdMapping( - { savedObjectsClient }, - mockUser1, - mockSession.id - ); - expect(searchIdMapping).toMatchInlineSnapshot(` - Map { - "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", - } - `); + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + const searchIdMapping = await service.getSearchIdMapping( + { savedObjectsClient }, + mockUser1, + mockSession.id + ); + expect(searchIdMapping).toMatchInlineSnapshot(` + Map { + "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", + } + `); + }); }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index c95c58a8dc06b..b5f7da594d53b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -29,6 +29,7 @@ import { TaskManagerStartContract, } from '../../../../task_manager/server'; import { + ENHANCED_ES_SEARCH_STRATEGY, SearchSessionRequestInfo, SearchSessionSavedObjectAttributes, SearchSessionStatus, @@ -36,8 +37,13 @@ import { } from '../../../common'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; -import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; +import { + registerSearchSessionsTask, + scheduleSearchSessionsTasks, + unscheduleSearchSessionsTask, +} from './monitoring_task'; import { SearchSessionsConfig, SearchStatus } from './types'; +import { DataEnhancedStartDependencies } from '../../type'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -78,7 +84,7 @@ export class SearchSessionService this.sessionConfig = this.config.search.sessions; } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { config: this.config, taskManager: deps.taskManager, @@ -99,6 +105,8 @@ export class SearchSessionService this.logger, this.sessionConfig.trackingInterval ); + } else { + unscheduleSearchSessionsTask(deps.taskManager, this.logger); } }; @@ -217,6 +225,7 @@ export class SearchSessionService restoreState = {}, }: Partial ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); @@ -316,6 +325,7 @@ export class SearchSessionService attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, @@ -353,6 +363,7 @@ export class SearchSessionService user: AuthenticatedUser | null, sessionId: string ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); this.logger.debug(`delete | ${sessionId}`); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); @@ -367,9 +378,9 @@ export class SearchSessionService user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, searchId: string, - { sessionId, strategy }: ISearchOptions + { sessionId, strategy = ENHANCED_ES_SEARCH_STRATEGY }: ISearchOptions ) => { - if (!sessionId || !searchId) return; + if (!this.sessionConfig.enabled || !sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); let idMapping: Record = {}; @@ -378,7 +389,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); const searchInfo = { id: searchId, - strategy: strategy!, + strategy, status: SearchStatus.IN_PROGRESS, }; idMapping = { [requestHash]: searchInfo }; @@ -411,7 +422,9 @@ export class SearchSessionService searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions ) => { - if (!sessionId) { + if (!this.sessionConfig.enabled) { + throw new Error('Search sessions are disabled'); + } else if (!sessionId) { throw new Error('Session ID is required'); } else if (!isStored) { throw new Error('Cannot get search ID from a session that is not stored'); diff --git a/x-pack/plugins/data_enhanced/server/type.ts b/x-pack/plugins/data_enhanced/server/type.ts index c4a16eab1a3a7..215700c5dcc5c 100644 --- a/x-pack/plugins/data_enhanced/server/type.ts +++ b/x-pack/plugins/data_enhanced/server/type.ts @@ -7,6 +7,13 @@ import type { IRouter } from 'kibana/server'; import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; +import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; /** * @internal @@ -17,3 +24,15 @@ export type DataEnhancedRequestHandlerContext = DataRequestHandlerContext; * @internal */ export type DataEnhancedPluginRouter = IRouter; + +export interface DataEnhancedSetupDependencies { + data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; + taskManager: TaskManagerSetupContract; + security?: SecurityPluginSetup; +} + +export interface DataEnhancedStartDependencies { + data: DataPluginStart; + taskManager: TaskManagerStartContract; +} From 4d43a4f31d1824ce6ed7c721e8f6fcee36349f59 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 7 Apr 2021 14:49:44 +0200 Subject: [PATCH 057/131] [Rollup] Migrate to new ES client (#95926) * initial pass at es client migration * fixed potential for not passing in an error message and triggering an unhandled exception * reworked ad hoc fixing of error response * delete legacy client file and remove use of legacyEs service * remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../errors/handle_es_error.ts | 2 +- .../server/client/elasticsearch_rollup.ts | 142 ------------------ x-pack/plugins/rollup/server/plugin.ts | 25 +-- .../routes/api/indices/register_get_route.ts | 17 +-- .../register_validate_index_pattern_route.ts | 50 +++--- .../routes/api/jobs/register_create_route.ts | 12 +- .../routes/api/jobs/register_delete_route.ts | 27 ++-- .../routes/api/jobs/register_get_route.ts | 10 +- .../routes/api/jobs/register_start_route.ts | 12 +- .../routes/api/jobs/register_stop_route.ts | 12 +- .../api/search/register_search_route.ts | 20 +-- .../plugins/rollup/server/services/license.ts | 7 +- .../plugins/rollup/server/shared_imports.ts | 2 +- x-pack/plugins/rollup/server/types.ts | 27 +--- .../apis/management/rollup/lib/es_index.js | 2 +- .../apps/rollup_job/hybrid_index_pattern.js | 14 +- .../functional/apps/rollup_job/rollup_jobs.js | 2 +- .../test/functional/apps/rollup_job/tsvb.js | 7 +- 18 files changed, 88 insertions(+), 302 deletions(-) delete mode 100644 x-pack/plugins/rollup/server/client/elasticsearch_rollup.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 a98a74375638d..42e18b72057ce 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 @@ -36,7 +36,7 @@ export const handleEsError = ({ return response.customError({ statusCode, body: { - message: body.error?.reason, + message: body.error?.reason ?? error.message ?? 'Unknown error', attributes: { // The full original ES error object error: body.error, diff --git a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts deleted file mode 100644 index 0296428c49613..0000000000000 --- a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.rollup = components.clientAction.namespaceFactory(); - const rollup = Client.prototype.rollup.prototype; - - rollup.rollupIndexCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_rollup/data', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.search = ca({ - urls: [ - { - fmt: '/<%=index%>/_rollup_search', - req: { - index: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - rollup.fieldCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_field_caps?fields=*', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.jobs = ca({ - urls: [ - { - fmt: '/_rollup/job/_all', - }, - ], - method: 'GET', - }); - - rollup.job = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.startJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_start', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.stopJob = ca({ - params: { - waitForCompletion: { - type: 'boolean', - name: 'wait_for_completion', - }, - }, - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_stop', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.deleteJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - rollup.createJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); -}; diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 1b982ab45205d..ff6adc1c8d24b 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -19,25 +19,16 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { PLUGIN, CONFIG_ROLLUPS } from '../common'; -import { Dependencies, RollupHandlerContext } from './types'; +import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { registerRollupUsageCollector } from './collectors'; import { rollupDataEnricher } from './rollup_data_enricher'; import { IndexPatternsFetcher } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; import { getCapabilitiesForRollupIndices } from '../../../../src/plugins/data/server'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - // Extend the elasticsearchJs client with additional endpoints. - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - - return core.elasticsearch.legacy.createClient('rollup', esClientConfig); -} - export class RollupPlugin implements Plugin { private readonly logger: Logger; private readonly globalConfig$: Observable; @@ -82,21 +73,11 @@ export class RollupPlugin implements Plugin { ], }); - http.registerRouteHandlerContext( - 'rollup', - async (context, request) => { - this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.rollupEsClient.asScoped(request), - }; - } - ); - registerApiRoutes({ router: http.createRouter(), license: this.license, lib: { - isEsError, + handleEsError, formatEsError, getCapabilitiesForRollupIndices, }, diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index 694ab3c467c1f..1d3be4b8e1fbb 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices }, + lib: { handleEsError, getCapabilitiesForRollupIndices }, }: RouteDependencies) => { router.get( { @@ -23,18 +23,13 @@ export const registerGetRoute = ({ }, license.guardApiRoute(async (context, request, response) => { try { - const data = await context.rollup!.client.callAsCurrentUser( - 'rollup.rollupIndexCapabilities', - { - indexPattern: '_all', - } - ); + const { client: clusterClient } = context.core.elasticsearch; + const { body: data } = await clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ + index: '_all', + }); return response.ok({ body: getCapabilitiesForRollupIndices(data) }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts index 90eabaa88b641..b2431c3838234 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -32,10 +32,6 @@ interface FieldCapability { scaled_float?: any; } -interface FieldCapabilities { - fields: FieldCapability[]; -} - function isNumericField(fieldCapability: FieldCapability) { const numericTypes = [ 'long', @@ -59,7 +55,7 @@ function isNumericField(fieldCapability: FieldCapability) { export const registerValidateIndexPatternRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -71,16 +67,12 @@ export const registerValidateIndexPatternRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities]: [ - FieldCapabilities, - { [key: string]: any } - ] = await Promise.all([ - context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }), - context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { - indexPattern, - }), + const [{ body: fieldCapabilities }, { body: rollupIndexCapabilities }] = await Promise.all([ + clusterClient.asCurrentUser.fieldCaps({ index: indexPattern, fields: '*' }), + clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ index: indexPattern }), ]); const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; @@ -92,23 +84,21 @@ export const registerValidateIndexPatternRoute = ({ const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - fieldCapabilitiesEntries.forEach( - ([fieldName, fieldCapability]: [string, FieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } + fieldCapabilitiesEntries.forEach(([fieldName, fieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } + if (fieldCapability.keyword) { + keywordFields.push(fieldName); } - ); + }); const body = { doesMatchIndices, @@ -132,11 +122,7 @@ export const registerValidateIndexPatternRoute = ({ return response.ok({ body: notFoundBody }); } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index bcb3a337aa725..11cfaf8851d45 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerCreateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.put( { @@ -29,21 +29,19 @@ export const registerCreateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { id, ...rest } = request.body.job; // Create job. - await context.rollup!.client.callAsCurrentUser('rollup.createJob', { + await clusterClient.asCurrentUser.rollup.putJob({ id, body: rest, }); // Then request the newly created job. - const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id }); + const { body: results } = await clusterClient.asCurrentUser.rollup.getJobs({ id }); return response.ok({ body: results.jobs[0] }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts index 4bbe73753e96c..f90a81f73823e 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerDeleteRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -24,28 +24,29 @@ export const registerDeleteRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.deleteJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { // There is an issue opened on ES to handle the following error correctly // https://github.com/elastic/elasticsearch/issues/42908 // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); + if ( + err?.meta && + err.body?.task_failures[0]?.reason?.reason?.includes( + 'Job must be [STOPPED] before deletion' + ) + ) { + err.meta.status = 400; + err.meta.statusCode = 400; + err.meta.displayName = 'Bad request'; + err.message = err.body.task_failures[0].reason.reason; } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts index a9a30c0370c5f..9944df2e55919 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -11,7 +11,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -19,14 +19,12 @@ export const registerGetRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { - const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs'); + const { body: data } = await clusterClient.asCurrentUser.rollup.getJobs({ id: '_all' }); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts index 2ebfcc437f41e..133c0cb34c9f5 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStartRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -29,20 +29,16 @@ export const registerStartRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.startJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.startJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts index faaf377a2d833..164273f604b43 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStopRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,23 +27,21 @@ export const registerStopRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; // For our API integration tests we need to wait for the jobs to be stopped // in order to be able to delete them sequentially. const { waitForCompletion } = request.query; const stopRollupJob = (id: string) => - context.rollup!.client.callAsCurrentUser('rollup.stopJob', { + clusterClient.asCurrentUser.rollup.stopJob({ id, - waitForCompletion: waitForCompletion === 'true', + wait_for_completion: waitForCompletion === 'true', }); const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts index f77ae7829bb6c..62aec4e01eaa0 100644 --- a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerSearchRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,21 +27,21 @@ export const registerSearchRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const requests = request.body.map(({ index, query }: { index: string; query?: any }) => - context.rollup.client.callAsCurrentUser('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) + clusterClient.asCurrentUser.rollup + .rollupSearch({ + index, + rest_total_hits_as_int: true, + body: query, + }) + .then(({ body }) => body) ); const data = await Promise.all(requests); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts index d2c3ff82eab1c..1b88a4020afa6 100644 --- a/x-pack/plugins/rollup/server/services/license.ts +++ b/x-pack/plugins/rollup/server/services/license.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { Logger } from 'src/core/server'; +import { Logger, RequestHandlerContext } from 'src/core/server'; import { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'src/core/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { RollupHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -57,11 +56,11 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: RollupHandlerContext, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts index 2167558c39652..fe157644c6b3d 100644 --- a/x-pack/plugins/rollup/server/shared_imports.ts +++ b/x-pack/plugins/rollup/server/shared_imports.ts @@ -7,4 +7,4 @@ export { IndexPatternsFetcher } from '../../../../src/plugins/data/server'; -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 45dcc976b211f..c774644da46ce 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; @@ -15,7 +15,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; export interface Dependencies { @@ -27,10 +27,10 @@ export interface Dependencies { } export interface RouteDependencies { - router: RollupPluginRouter; + router: IRouter; license: License; lib: { - isEsError: typeof isEsError; + handleEsError: typeof handleEsError; formatEsError: typeof formatEsError; getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; }; @@ -38,22 +38,3 @@ export interface RouteDependencies { IndexPatternsFetcher: typeof IndexPatternsFetcher; }; } - -/** - * @internal - */ -interface RollupApiRequestHandlerContext { - client: ILegacyScopedClusterClient; -} - -/** - * @internal - */ -export interface RollupHandlerContext extends RequestHandlerContext { - rollup: RollupApiRequestHandlerContext; -} - -/** - * @internal - */ -export type RollupPluginRouter = IRouter; diff --git a/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js b/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js index d4c6be0f4a0dc..7aacd9f6a7fc6 100644 --- a/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js +++ b/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js @@ -13,7 +13,7 @@ import { getRandomString } from './random'; * @param {ElasticsearchClient} es The Elasticsearch client instance */ export const initElasticsearchIndicesHelpers = (getService) => { - const es = getService('legacyEs'); + const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); let indicesCreated = []; diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index 4fd7c2cc2f067..1eb2862901277 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import mockRolledUpData, { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const find = getService('find'); const retry = getService('retry'); @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }) { 'waiting for 3 records to be loaded into elasticsearch.', 10000, async () => { - const response = await es.indices.get({ + const { body: response } = await es.indices.get({ index: `${rollupSourceIndexPrefix}*`, allow_no_indices: false, }); @@ -53,9 +53,8 @@ export default function ({ getService, getPageObjects }) { await retry.try(async () => { //Create a rollup for kibana to recognize - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'PUT', + await es.rollup.putJob({ + id: rollupJobName, body: { index_pattern: `${rollupSourceIndexPrefix}*`, rollup_index: rollupTargetIndexName, @@ -104,10 +103,7 @@ export default function ({ getService, getPageObjects }) { after(async () => { // Delete the rollup job. - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'DELETE', - }); + await es.rollup.deleteJob({ id: rollupJobName }); await esDeleteAllIndices([ rollupTargetIndexName, diff --git a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js index 60a878f343c5c..a0684a77748b7 100644 --- a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js +++ b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['rollup', 'common', 'security']); const security = getService('security'); diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index aebea93f1e4bf..d0c7c86d6d5c3 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import mockRolledUpData from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -49,9 +49,8 @@ export default function ({ getService, getPageObjects }) { await retry.try(async () => { //Create a rollup for kibana to recognize - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'PUT', + await es.rollup.putJob({ + id: rollupJobName, body: { index_pattern: rollupSourceIndexName, rollup_index: rollupTargetIndexName, From 64470829de7f8cbe8d4f577c9f983cce37c33e0a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 7 Apr 2021 15:17:04 +0200 Subject: [PATCH 058/131] [APM] Optimized type checking for API tests (#96388) Closes #95634. --- .../apm/scripts/optimize-tsconfig/optimize.js | 26 ++++++++++++++++++- .../apm/scripts/optimize-tsconfig/paths.js | 3 +++ .../optimize-tsconfig/test-tsconfig.json | 16 ++++++++++++ x-pack/plugins/apm/scripts/precommit.js | 24 ++++++++++------- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index fed938119c4a6..58fb096ca3a51 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -18,7 +18,12 @@ const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const unlink = promisify(fs.unlink); -const { kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); +const { + kibanaRoot, + tsconfigTpl, + tsconfigTplTest, + filesToIgnore, +} = require('./paths'); const { unoptimizeTsConfig } = require('./unoptimize'); async function prepareBaseTsConfig() { @@ -57,6 +62,23 @@ async function addApmFilesToRootTsConfig() { ); } +async function addApmFilesToTestTsConfig() { + const template = json5.parse(await readFile(tsconfigTplTest, 'utf-8')); + const testTsConfigFilename = path.join( + kibanaRoot, + 'x-pack/test/tsconfig.json' + ); + const testTsConfig = json5.parse( + await readFile(testTsConfigFilename, 'utf-8') + ); + + await writeFile( + testTsConfigFilename, + JSON.stringify({ ...testTsConfig, ...template, references: [] }, null, 2), + { encoding: 'utf-8' } + ); +} + async function setIgnoreChanges() { for (const filename of filesToIgnore) { await execa('git', ['update-index', '--skip-worktree', filename]); @@ -74,6 +96,8 @@ async function optimizeTsConfig() { await addApmFilesToRootTsConfig(); + await addApmFilesToTestTsConfig(); + await deleteApmTsConfig(); await setIgnoreChanges(); diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index dbc207c9e6d26..bde129f434934 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -9,15 +9,18 @@ const path = require('path'); const kibanaRoot = path.resolve(__dirname, '../../../../..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); +const tsconfigTplTest = path.resolve(__dirname, './test-tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), + path.resolve(kibanaRoot, 'x-pack/test', 'tsconfig.json'), ]; module.exports = { kibanaRoot, tsconfigTpl, + tsconfigTplTest, filesToIgnore, }; diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json new file mode 100644 index 0000000000000..d6718b7511179 --- /dev/null +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "types": [ + "node" + ], + "noErrorTruncation": true + }, + "include": [ + "./apm_api_integration/**/*", + "../../packages/kbn-test/types/**/*", + "../../typings/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index c7102ce913f01..695a9ba70f5d7 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -23,6 +23,8 @@ const tsconfig = useOptimizedTsConfig ? resolve(root, 'tsconfig.json') : resolve(root, 'x-pack/plugins/apm/tsconfig.json'); +const testTsconfig = resolve(root, 'x-pack/test/tsconfig.json'); + const tasks = new Listr( [ { @@ -55,16 +57,18 @@ const tasks = new Listr( ], execaOpts ).then(() => - execa( - require.resolve('typescript/bin/tsc'), - [ - '--project', - tsconfig, - '--pretty', - ...(useOptimizedTsConfig ? ['--noEmit'] : []), - ], - execaOpts - ) + Promise.all([ + execa( + require.resolve('typescript/bin/tsc'), + ['--project', tsconfig, '--pretty', '--noEmit'], + execaOpts + ), + execa( + require.resolve('typescript/bin/tsc'), + ['--project', testTsconfig, '--pretty', '--noEmit'], + execaOpts + ), + ]) ), }, { From 6fa23eb61958d82e1abe351e6f7056fa1bd1e5cd Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 7 Apr 2021 16:14:26 +0200 Subject: [PATCH 059/131] Add docs for v2 migration timeouts related to fleet-agent-events (#95370) --- docs/setup/upgrade/upgrade-migrations.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 8603ca9935cac..fdcd71791ad3a 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -50,6 +50,16 @@ For large deployments with more than 10 {kib} instances and more than 10 000 sav ==== Preventing migration failures This section highlights common causes of {kib} upgrade failures and how to prevent them. +[float] +===== timeout_exception or receive_timeout_transport_exception +There is a known issue in v7.12.0 for users who tried the fleet beta. Upgrade migrations fail because of a large number of documents in the `.kibana` index. + +This can cause Kibana to log errors like: +> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms] +> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] + +See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. From ba84a7105e1db9780c74a129199ffd80d4da2be6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 7 Apr 2021 16:30:34 +0200 Subject: [PATCH 060/131] Deprecate migrations.enableV2 (#96398) * deprecate migrations.enableV2 * provide deprecation test tool from core * use deprecation test tool in tests * add a test for SO depreacations --- .../deprecation/core_deprecations.test.ts | 21 +------ src/core/server/config/test_utils.ts | 52 ++++++++++++++++++ .../elasticsearch_config.test.ts | 26 +++------ src/core/server/kibana_config.test.ts | 26 +++------ .../saved_objects_config.test.ts | 44 +++++++++++++++ .../saved_objects/saved_objects_config.ts | 55 +++++++++++++------ src/core/server/test_utils.ts | 1 + 7 files changed, 151 insertions(+), 74 deletions(-) create mode 100644 src/core/server/config/test_utils.ts create mode 100644 src/core/server/saved_objects/saved_objects_config.test.ts diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index e3c236405a596..a8063c317b3c5 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -6,27 +6,12 @@ * Side Public License, v 1. */ -import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; +import { getDeprecationsForGlobalSettings } from '../test_utils'; import { coreDeprecationProvider } from './core_deprecations'; - const initialEnv = { ...process.env }; -const applyCoreDeprecations = (settings: Record = {}) => { - const deprecations = coreDeprecationProvider(configDeprecationFactory); - const deprecationMessages: string[] = []; - const migrated = applyDeprecations( - settings, - deprecations.map((deprecation) => ({ - deprecation, - path: '', - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyCoreDeprecations = (settings?: Record) => + getDeprecationsForGlobalSettings({ provider: coreDeprecationProvider, settings }); describe('core deprecations', () => { beforeEach(() => { diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts new file mode 100644 index 0000000000000..2eaf462768724 --- /dev/null +++ b/src/core/server/config/test_utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { ConfigDeprecationProvider } from '@kbn/config'; +import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; + +function collectDeprecations( + provider: ConfigDeprecationProvider, + settings: Record, + path: string +) { + const deprecations = provider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +} + +export const getDeprecationsFor = ({ + provider, + settings = {}, + path, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; + path: string; +}) => { + return collectDeprecations(provider, { [path]: settings }, path); +}; + +export const getDeprecationsForGlobalSettings = ({ + provider, + settings = {}, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; +}) => { + return collectDeprecations(provider, settings, ''); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 23b804b535405..f8ef1a7a20a83 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -12,29 +12,17 @@ import { mockReadPkcs12Truststore, } from './elasticsearch_config.test.mocks'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; import { ElasticsearchConfig, config } from './elasticsearch_config'; +import { getDeprecationsFor } from '../config/test_utils'; const CONFIG_PATH = 'elasticsearch'; -const applyElasticsearchDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyElasticsearchDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); test('set correct defaults', () => { const configValue = new ElasticsearchConfig(config.schema.validate({})); diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts index 1acdff9dd78e6..47bb6cf2a064c 100644 --- a/src/core/server/kibana_config.test.ts +++ b/src/core/server/kibana_config.test.ts @@ -7,28 +7,16 @@ */ import { config } from './kibana_config'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; +import { getDeprecationsFor } from './config/test_utils'; const CONFIG_PATH = 'kibana'; -const applyKibanaDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyKibanaDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); it('set correct defaults ', () => { const configValue = config.schema.validate({}); diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts new file mode 100644 index 0000000000000..720b28403edf2 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_config.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { savedObjectsMigrationConfig } from './saved_objects_config'; +import { getDeprecationsFor } from '../config/test_utils'; + +const applyMigrationsDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: savedObjectsMigrationConfig.deprecations!, + settings, + path: 'migrations', + }); + +describe('migrations config', function () { + describe('deprecations', () => { + it('logs a warning if migrations.enableV2 is set: true', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: true }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + + it('logs a warning if migrations.enableV2 is set: false', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + }); + + it('does not log a warning if migrations.enableV2 is not set', () => { + const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 }); + expect(messages).toMatchInlineSnapshot(`Array []`); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 96fac85ded076..7182df74c597f 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -7,31 +7,50 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import type { ServiceConfigDescriptor } from '../internal_types'; +import type { ConfigDeprecationProvider } from '../config'; -export type SavedObjectsMigrationConfigType = TypeOf; +const migrationSchema = schema.object({ + batchSize: schema.number({ defaultValue: 1_000 }), + scrollDuration: schema.string({ defaultValue: '15m' }), + pollInterval: schema.number({ defaultValue: 1_500 }), + skip: schema.boolean({ defaultValue: false }), + enableV2: schema.boolean({ defaultValue: true }), + retryAttempts: schema.number({ defaultValue: 15 }), +}); -export const savedObjectsMigrationConfig = { +export type SavedObjectsMigrationConfigType = TypeOf; + +const migrationDeprecations: ConfigDeprecationProvider = () => [ + (settings, fromPath, addDeprecation) => { + const migrationsConfig = settings[fromPath]; + if (migrationsConfig?.enableV2 !== undefined) { + addDeprecation({ + message: + '"migrations.enableV2" is deprecated and will be removed in an upcoming release without any further notice.', + documentationUrl: 'https://ela.st/kbn-so-migration-v2', + }); + } + return settings; + }, +]; + +export const savedObjectsMigrationConfig: ServiceConfigDescriptor = { path: 'migrations', - schema: schema.object({ - batchSize: schema.number({ defaultValue: 1000 }), - scrollDuration: schema.string({ defaultValue: '15m' }), - pollInterval: schema.number({ defaultValue: 1500 }), - skip: schema.boolean({ defaultValue: false }), - // TODO migrationsV2: remove/deprecate once we release migrations v2 - enableV2: schema.boolean({ defaultValue: true }), - /** the number of times v2 migrations will retry temporary failures such as a timeout, 503 status code or snapshot_in_progress_exception */ - retryAttempts: schema.number({ defaultValue: 15 }), - }), + schema: migrationSchema, + deprecations: migrationDeprecations, }; -export type SavedObjectsConfigType = TypeOf; +const soSchema = schema.object({ + maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), + maxImportExportSize: schema.number({ defaultValue: 10_000 }), +}); + +export type SavedObjectsConfigType = TypeOf; -export const savedObjectsConfig = { +export const savedObjectsConfig: ServiceConfigDescriptor = { path: 'savedObjects', - schema: schema.object({ - maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), - maxImportExportSize: schema.number({ defaultValue: 10_000 }), - }), + schema: soSchema, }; export class SavedObjectConfig { diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index 656d2bfe60fac..cf18defb0a960 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -9,3 +9,4 @@ export { createHttpServer } from './http/test_utils'; export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; export { setupServer } from './saved_objects/routes/test_utils'; +export { getDeprecationsFor, getDeprecationsForGlobalSettings } from './config/test_utils'; From 7f4ec48ce63aa41c852ad6bcd9796d409a6e9fe9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 7 Apr 2021 10:32:20 -0400 Subject: [PATCH 061/131] [ML] Data Frame Analytics: add accuracy and recall stats to results view (#96270) * add accuracy and recall to classification results * update accuracy tooltip content --- .../data_frame_analytics/common/analytics.ts | 17 +++++++ .../evaluate_panel.tsx | 49 ++++++++++++++++++- .../evaluate_stat.tsx | 44 +++++++++++++++++ .../use_confusion_matrix.ts | 6 ++- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 0f1b50a7b9316..505673f440ef2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -160,11 +160,24 @@ export interface RocCurveItem { tpr: number; } +interface EvalClass { + class_name: string; + value: number; +} + export interface ClassificationEvaluateResponse { classification: { multiclass_confusion_matrix?: { confusion_matrix: ConfusionMatrix[]; }; + recall?: { + classes: EvalClass[]; + avg_recall: number; + }; + accuracy?: { + classes: EvalClass[]; + overall_accuracy: number; + }; auc_roc?: { curve?: RocCurveItem[]; value: number; @@ -434,6 +447,8 @@ export enum REGRESSION_STATS { interface EvaluateMetrics { classification: { + accuracy?: object; + recall?: object; multiclass_confusion_matrix?: object; auc_roc?: { include_curve: boolean; class_name: string }; }; @@ -486,6 +501,8 @@ export const loadEvalData = async ({ const metrics: EvaluateMetrics = { classification: { + accuracy: {}, + recall: {}, ...(includeMulticlassConfusionMatrix ? { multiclass_confusion_matrix: {} } : {}), ...(rocCurveClassName !== undefined ? { auc_roc: { include_curve: true, class_name: rocCurveClassName } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index e848f209516f4..3795af32f6638 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -34,6 +34,7 @@ import { DataFrameTaskStateType } from '../../../analytics_management/components import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; +import { EvaluateStat } from './evaluate_stat'; import { getRocCurveChartVegaLiteSpec } from './get_roc_curve_chart_vega_lite_spec'; @@ -112,10 +113,12 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se const isTraining = isTrainingFilter(searchQuery, resultsField); const { + avgRecall, confusionMatrixData, docsCount, error: errorConfusionMatrix, isLoading: isLoadingConfusionMatrix, + overallAccuracy, } = useConfusionMatrix(jobConfig, searchQuery); useEffect(() => { @@ -368,8 +371,52 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se )} ) : null} + {/* Accuracy and Recall */} + + + + + + + + + {/* AUC ROC Chart */} - + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx new file mode 100644 index 0000000000000..4bb8415d833f6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EMPTY_STAT } from '../../../../common/analytics'; + +interface Props { + isLoading: boolean; + title: number | null; + description: string; + dataTestSubj: string; + tooltipContent: string; +} + +export const EvaluateStat: FC = ({ + isLoading, + title, + description, + dataTestSubj, + tooltipContent, +}) => ( + + + + + + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index be44a8e36ed00..df48d2c5ab44f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -30,6 +30,8 @@ export const useConfusionMatrix = ( searchQuery: ResultsSearchQuery ) => { const [confusionMatrixData, setConfusionMatrixData] = useState([]); + const [overallAccuracy, setOverallAccuracy] = useState(null); + const [avgRecall, setAvgRecall] = useState(null); const [isLoading, setIsLoading] = useState(false); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); @@ -77,6 +79,8 @@ export const useConfusionMatrix = ( evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix; setError(null); setConfusionMatrixData(confusionMatrix || []); + setAvgRecall(evalData.eval?.classification?.recall?.avg_recall || null); + setOverallAccuracy(evalData.eval?.classification?.accuracy?.overall_accuracy || null); setIsLoading(false); } else { setIsLoading(false); @@ -94,5 +98,5 @@ export const useConfusionMatrix = ( loadConfusionMatrixData(); }, [JSON.stringify([jobConfig, searchQuery])]); - return { confusionMatrixData, docsCount, error, isLoading }; + return { avgRecall, confusionMatrixData, docsCount, error, isLoading, overallAccuracy }; }; From f945f3a425120c55c0b8ed4666899966e68059ac Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 7 Apr 2021 16:39:11 +0200 Subject: [PATCH 062/131] [ML] Transforms: Wizard displays warning callout for source preview when used with CCS against clusters below 7.10. (#96297) The transforms UI source preview uses fields to search and retrieve document attributes. The feature was introduced in 7.10. For cross cluster search, as of now, when a search using fields is used using cross cluster search against a cluster earlier than 7.10, the API won't return an error or other information but just silently drop the fields attribute and return empty hits without field attributes. In Kibana, index patterns can be set up to use cross cluster search using the pattern :. If we identify such a pattern and the search hits don't include fields attributes, we display a warning callout from now on. --- .../components/data_grid/data_grid.tsx | 19 +++++++ .../application/components/data_grid/types.ts | 3 ++ .../components/data_grid/use_data_grid.tsx | 3 ++ .../public/app/hooks/__mocks__/use_api.ts | 13 ++++- .../public/app/hooks/use_index_data.test.tsx | 54 +++++++++++++++++-- .../public/app/hooks/use_index_data.ts | 5 ++ .../step_define/step_define_summary.test.tsx | 11 ++-- 7 files changed, 98 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 88f1c0226cb37..2a851eeccdce6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -80,6 +80,7 @@ export const DataGrid: FC = memo( baseline, chartsVisible, chartsButtonVisible, + ccsWarning, columnsWithCharts, dataTestSubj, errorMessage, @@ -291,6 +292,24 @@ export const DataGrid: FC = memo(
)} + {ccsWarning && ( +
+ +

+ {i18n.translate('xpack.ml.dataGrid.CcsWarningCalloutBody', { + defaultMessage: + 'There was an issue retrieving data for the index pattern. Source preview in combination with cross-cluster search is only supported for versions 7.10 and above. You may still configure and create the transform.', + })} +

+
+ +
+ )}
void; rowCount: number; rowCountRelation: RowCountRelation; + setCcsWarning: Dispatch>; setColumnCharts: Dispatch>; setErrorMessage: Dispatch>; setNoDataMessage: Dispatch>; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index e62f2eb2f003b..633c3d9aab002 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -36,6 +36,7 @@ export const useDataGrid = ( ): UseDataGridReturnType => { const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + const [ccsWarning, setCcsWarning] = useState(false); const [noDataMessage, setNoDataMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); @@ -152,6 +153,7 @@ export const useDataGrid = ( }, [chartsVisible, rowCount, rowCountRelation]); return { + ccsWarning, chartsVisible, chartsButtonVisible: true, columnsWithCharts, @@ -166,6 +168,7 @@ export const useDataGrid = ( rowCount, rowCountRelation, setColumnCharts, + setCcsWarning, setErrorMessage, setNoDataMessage, setPagination, diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index a9455877be429..3d5e1783f8c62 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -136,9 +136,20 @@ const apiFactory = () => ({ return Promise.resolve([]); }, async esSearch(payload: any): Promise { + const hits = []; + + // simulate a cross cluster search result + // against a cluster that doesn't support fields + if (payload.index.includes(':')) { + hits.push({ + _id: 'the-doc', + _index: 'the-index', + }); + } + return Promise.resolve({ hits: { - hits: [], + hits, total: { value: 0, relation: 'eq', diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index bd361afac2d8d..3e0a247106f2a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -7,7 +7,8 @@ import React, { FC } from 'react'; -import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { render, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { CoreSetup } from 'src/core/public'; @@ -49,6 +50,7 @@ describe('Transform: useIndexData()', () => { const wrapper: FC = ({ children }) => ( {children} ); + const { result, waitForNextUpdate } = renderHook( () => useIndexData( @@ -62,6 +64,7 @@ describe('Transform: useIndexData()', () => { ), { wrapper } ); + const IndexObj: UseIndexDataReturnType = result.current; await waitForNextUpdate(); @@ -73,7 +76,7 @@ describe('Transform: useIndexData()', () => { }); describe('Transform: with useIndexData()', () => { - test('Minimal initialization', async () => { + test('Minimal initialization, no cross cluster search warning.', async () => { // Arrange const indexPattern = { title: 'the-index-pattern-title', @@ -97,7 +100,47 @@ describe('Transform: with useIndexData()', () => { return ; }; - const { getByText } = render( + + const { queryByText } = render( + + + + ); + + // Act + // Assert + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).not.toBeInTheDocument(); + }); + }); + + test('Cross-cluster search warning', async () => { + // Arrange + const indexPattern = { + title: 'remote:the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const mlSharedImports = await getMlSharedImports(); + + const Wrapper = () => { + const { + ml: { DataGrid }, + } = useAppDependencies(); + const props = { + ...useIndexData(indexPattern, { match_all: {} }, runtimeMappings), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + + const { queryByText } = render( @@ -105,6 +148,9 @@ describe('Transform: with useIndexData()', () => { // Act // Assert - expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 36ba07afd69cd..f97693b8c038a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -87,6 +87,7 @@ export const useIndexData = ( pagination, resetPagination, setColumnCharts, + setCcsWarning, setErrorMessage, setRowCount, setRowCountRelation, @@ -134,8 +135,12 @@ export const useIndexData = ( return; } + const isCrossClusterSearch = indexPattern.title.includes(':'); + const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined'); + const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + setCcsWarning(isCrossClusterSearch && isMissingFields); setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); setRowCountRelation( typeof resp.hits.total === 'number' diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 51d3a0bd02d50..1e3fa2026061b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, wait } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -77,7 +77,7 @@ describe('Transform: ', () => { }, }; - const { getByText } = render( + const { queryByText } = render( @@ -85,8 +85,9 @@ describe('Transform: ', () => { // Act // Assert - expect(getByText('Group by')).toBeInTheDocument(); - expect(getByText('Aggregations')).toBeInTheDocument(); - await wait(); + await waitFor(() => { + expect(queryByText('Group by')).toBeInTheDocument(); + expect(queryByText('Aggregations')).toBeInTheDocument(); + }); }); }); From bb109b533c71d4a477a07472806e90d01aa82f3f Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 7 Apr 2021 10:44:12 -0400 Subject: [PATCH 063/131] [Actions] Hiding time field selector if no field with date mapping in index in Index Connector flyout (#96080) * Hiding time field selector if no field with date mapping in index * Fixing types check * Updating tooltip * PR fixes --- .../es_index/es_index_connector.test.tsx | 313 ++++++++++++++---- .../es_index/es_index_connector.tsx | 100 +++--- 2 files changed, 305 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index 008fc8237c129..e9212bf633a79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from 'react-dom/test-utils'; import { EsIndexActionConnector } from '../types'; import IndexActionConnectorFields from './es_index_connector'; +import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/index_controls', () => ({ @@ -19,83 +20,263 @@ jest.mock('../../../../common/index_controls', () => ({ getIndexPatterns: jest.fn(), })); +const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); +getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, +]); + +const { getFields } = jest.requireMock('../../../../common/index_controls'); + +async function setup(props: any) { + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; +} + +function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) { + getFields.mockResolvedValueOnce([ + { + type: getFieldsWithDateMapping ? 'date' : 'keyword', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); +} describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); - getIndexPatterns.mockResolvedValueOnce([ - { - id: 'indexPattern1', - attributes: { - title: 'indexPattern1', - }, - }, - { - id: 'indexPattern2', - attributes: { - title: 'indexPattern2', - }, - }, - ]); - const { getFields } = jest.requireMock('../../../../common/index_controls'); - getFields.mockResolvedValueOnce([ - { - type: 'date', - name: 'test1', - }, - { - type: 'text', - name: 'test2', - }, - ]); - - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test', - refresh: false, - executionTimeField: 'test1', - }, - } as EsIndexActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - /> - ); + test('renders correctly when creating connector', async () => { + const props = { + action: { + actionTypeId: '.index', + config: {}, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time field switch shouldn't show up initially + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + + // time field switch should show up if index has date type field mapping + setupGetFieldsResponse(true); await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + // time field switch should go away if index does not has date type field mapping + setupGetFieldsResponse(false); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + // time field dropdown should show up if index has date type field mapping and time switch is clicked + setupGetFieldsResponse(true); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + await act(async () => { + timeFieldSwitch.prop('onChange')!(({ + target: { checked: true }, + } as unknown) as EuiSwitchEvent); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + }); + + test('renders correctly when editing connector - no date type field mapping', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time related fields shouldn't show up + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - refresh set to true', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: true, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(true); + }); + + test('renders correctly when editing connector - with date type field mapping but no time field selected', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - with date type field mapping and selected time field', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + executionTimeField: 'test1', + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); - const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); - expect(indexSearchBoxValue.first().props().value).toEqual(''); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); - const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); - indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox.find('input').first().simulate('change', event); + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); - const indexSearchBoxValueBeforeEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); - const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); - indexComboBoxClear.first().simulate('click'); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(true); - const indexSearchBoxValueAfterEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); + const timeFieldSelect = wrapper + .find(EuiSelect) + .filter('[data-test-subj="executionTimeFieldSelect"]'); + expect(timeFieldSelect.prop('value')).toEqual('test1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index cd3a03ecce15c..72af41277c29c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -30,29 +30,45 @@ import { } from '../../../../common/index_controls'; import { useKibana } from '../../../../common/lib/kibana'; +interface TimeFieldOptions { + value: string; + text: string; +} + const IndexActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, errors, readOnly }) => { const { http, docLinks } = useKibana().services; const { index, refresh, executionTimeField } = action.config; - const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( + const [showTimeFieldCheckbox, setShowTimeFieldCheckboxState] = useState( + executionTimeField != null + ); + const [hasTimeFieldCheckbox, setHasTimeFieldCheckboxState] = useState( executionTimeField != null ); const [indexPatterns, setIndexPatterns] = useState([]); const [indexOptions, setIndexOptions] = useState([]); - const [timeFieldOptions, setTimeFieldOptions] = useState>([ - firstFieldOption, - ]); + const [timeFieldOptions, setTimeFieldOptions] = useState([]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); + const setTimeFields = (fields: TimeFieldOptions[]) => { + if (fields.length > 0) { + setShowTimeFieldCheckboxState(true); + setTimeFieldOptions([firstFieldOption, ...fields]); + } else { + setHasTimeFieldCheckboxState(false); + setShowTimeFieldCheckboxState(false); + setTimeFieldOptions([]); + } + }; + useEffect(() => { const indexPatternsFunction = async () => { setIndexPatterns(await getIndexPatterns()); if (index) { const currentEsFields = await getFields(http!, [index]); - const timeFields = getTimeFieldOptions(currentEsFields as any); - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); } }; indexPatternsFunction(); @@ -123,13 +139,11 @@ const IndexActionConnectorFields: React.FunctionComponent< // reset time field and expression fields if indices are deleted if (indices.length === 0) { - setTimeFieldOptions([]); + setTimeFields([]); return; } const currentEsFields = await getFields(http!, indices); - const timeFields = getTimeFieldOptions(currentEsFields as any); - - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); }} onSearchChange={async (search) => { setIsIndiciesLoading(true); @@ -172,38 +186,40 @@ const IndexActionConnectorFields: React.FunctionComponent< } /> - { - setTimeFieldCheckboxState(!hasTimeFieldCheckbox); - // if changing from checked to not checked (hasTimeField === true), - // set time field to null - if (hasTimeFieldCheckbox) { - editActionConfig('executionTimeField', null); + {showTimeFieldCheckbox && ( + { + setHasTimeFieldCheckboxState(!hasTimeFieldCheckbox); + // if changing from checked to not checked (hasTimeField === true), + // set time field to null + if (hasTimeFieldCheckbox) { + editActionConfig('executionTimeField', null); + } + }} + label={ + <> + + + } - }} - label={ - <> - - - - } - /> - {hasTimeFieldCheckbox ? ( + /> + )} + {hasTimeFieldCheckbox && ( <> - ) : null} + )} ); }; From 76ed8dbeabd9235d0c9aa94f3e8abbdbf46862ac Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 7 Apr 2021 07:54:12 -0700 Subject: [PATCH 064/131] [Alerting UI] Changed alerting UIs use new rule APIs. (#96018) * [Alerting UI] Changed alerting UIs use new rule APIs. * added unit tests * fixed types * fixed types * fixed types * fixed due to comments --- .../components/health_check.test.tsx | 36 +- .../application/components/health_check.tsx | 2 +- .../public/application/constants/index.ts | 5 +- .../public/application/lib/alert_api.test.ts | 875 ------------------ .../public/application/lib/alert_api.ts | 296 ------ .../lib/alert_api/aggregate.test.ts | 212 +++++ .../application/lib/alert_api/aggregate.ts | 44 + .../lib/alert_api/alert_summary.test.ts | 58 ++ .../lib/alert_api/alert_summary.ts | 41 + .../lib/alert_api/common_transformations.ts | 61 ++ .../application/lib/alert_api/create.test.ts | 140 +++ .../application/lib/alert_api/create.ts | 44 + .../application/lib/alert_api/delete.test.ts | 32 + .../application/lib/alert_api/delete.ts | 28 + .../application/lib/alert_api/disable.test.ts | 47 + .../application/lib/alert_api/disable.ts | 22 + .../application/lib/alert_api/enable.test.ts | 47 + .../application/lib/alert_api/enable.ts | 22 + .../lib/alert_api/get_rule.test.ts | 99 ++ .../application/lib/alert_api/get_rule.ts | 21 + .../application/lib/alert_api/health.test.ts | 41 + .../application/lib/alert_api/health.ts | 44 + .../public/application/lib/alert_api/index.ts | 24 + .../lib/alert_api/map_filters_to_kql.test.ts | 68 ++ .../lib/alert_api/map_filters_to_kql.ts | 36 + .../application/lib/alert_api/mute.test.ts | 47 + .../public/application/lib/alert_api/mute.ts | 16 + .../lib/alert_api/mute_alert.test.ts | 25 + .../application/lib/alert_api/mute_alert.ts | 20 + .../lib/alert_api/rule_types.test.ts | 45 + .../application/lib/alert_api/rule_types.ts | 39 + .../application/lib/alert_api/rules.test.ts | 242 +++++ .../public/application/lib/alert_api/rules.ts | 59 ++ .../application/lib/alert_api/state.test.ts | 101 ++ .../public/application/lib/alert_api/state.ts | 49 + .../application/lib/alert_api/unmute.test.ts | 47 + .../application/lib/alert_api/unmute.ts | 22 + .../lib/alert_api/unmute_alert.test.ts | 25 + .../application/lib/alert_api/unmute_alert.ts | 20 + .../application/lib/alert_api/update.test.ts | 60 ++ .../application/lib/alert_api/update.ts | 52 ++ 41 files changed, 2033 insertions(+), 1181 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 3baf4e33fb68d..44c950a500040 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -59,8 +59,13 @@ describe('health check', () => { it('renders children if keys are enabled', async () => { useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, }); const { queryByText } = render( @@ -78,8 +83,13 @@ describe('health check', () => { test('renders warning if TLS is required', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: false, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryAllByText } = render( @@ -110,8 +120,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: true, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryByText, queryByRole } = render( @@ -139,8 +154,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral and keys are disabled', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: false, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 208fd5ec66f1d..d75ab102a8e0c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { DocLinksStart } from 'kibana/public'; -import { alertingFrameworkHealth } from '../lib/alert_api'; import './health_check.scss'; import { useHealthContext } from '../context/health_context'; import { useKibana } from '../../common/lib/kibana'; import { CenterJustifiedSpinner } from './center_justified_spinner'; import { triggersActionsUiHealth } from '../../common/lib/health_api'; +import { alertingFrameworkHealth } from '../lib/alert_api'; interface Props { inFlyout?: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 8ac1fbaec403b..cc04b8e7871cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; -export { LEGACY_BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { + BASE_ALERTING_API_PATH, + INTERNAL_BASE_ALERTING_API_PATH, +} from '../../../../alerting/common'; export { BASE_ACTION_API_PATH } from '../../../../actions/common'; export type Section = 'connectors' | 'rules'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts deleted file mode 100644 index d112e7ac284ae..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ /dev/null @@ -1,875 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Alert, AlertType, AlertUpdates } from '../../types'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; -import { - createAlert, - deleteAlerts, - disableAlerts, - enableAlerts, - disableAlert, - enableAlert, - loadAlert, - loadAlertAggregations, - loadAlerts, - loadAlertState, - loadAlertTypes, - muteAlerts, - unmuteAlerts, - muteAlert, - unmuteAlert, - updateAlert, - muteAlertInstance, - unmuteAlertInstance, - alertingFrameworkHealth, - mapFiltersToKql, -} from './alert_api'; -import uuid from 'uuid'; -import { AlertNotifyWhenType, ALERTS_FEATURE_ID } from '../../../../alerting/common'; - -const http = httpServiceMock.createStartContract(); - -beforeEach(() => jest.resetAllMocks()); - -describe('loadAlertTypes', () => { - test('should call get alert types API', async () => { - const resolvedValue: AlertType[] = [ - { - id: 'test', - name: 'Test', - actionVariables: { - context: [{ name: 'var1', description: 'val1' }], - state: [{ name: 'var2', description: 'val2' }], - params: [{ name: 'var3', description: 'val3' }], - }, - producer: ALERTS_FEATURE_ID, - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - defaultActionGroupId: 'default', - authorizedConsumers: {}, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }, - ]; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertTypes({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/list_alert_types", - ] - `); - }); -}); - -describe('loadAlert', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - id: alertId, - name: 'name', - tags: [], - enabled: true, - alertTypeId: '.noop', - schedule: { interval: '1s' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); - }); -}); - -describe('loadAlertState', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: {}, - second_instance: {}, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should parse AlertInstances', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: '2020-02-09T23:15:41.941Z', - }, - }, - }, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual({ - ...resolvedValue, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date('2020-02-09T23:15:41.941Z'), - }, - }, - }, - }, - }); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should handle empty response from api', async () => { - const alertId = uuid.v4(); - http.get.mockResolvedValueOnce(''); - - expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); -}); - -describe('loadAlerts', () => { - test('should call find API with base parameters', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'foo', - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText and tagsFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'apples, foo, baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "apples, foo, baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); -}); - -describe('loadAlertAggregations', () => { - test('should call aggregate API with base parameters', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with searchText', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http, searchText: 'apples' }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'foo', - actionTypesFilter: ['action', 'type'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'baz', - actionTypesFilter: ['action', 'type'], - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); -}); - -describe('deleteAlerts', () => { - test('should call delete API for each alert', async () => { - const ids = ['1', '2', '3']; - const result = await deleteAlerts({ http, ids }); - expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); - expect(http.delete.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1", - ], - Array [ - "/api/alerts/alert/2", - ], - Array [ - "/api/alerts/alert/3", - ], - ] - `); - }); -}); - -describe('createAlert', () => { - test('should call create alert API', async () => { - const alertToCreate: AlertUpdates = { - name: 'test', - consumer: 'alerts', - tags: ['foo'], - enabled: true, - alertTypeId: 'test', - schedule: { - interval: '1m', - }, - actions: [], - params: {}, - throttle: null, - notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType, - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKeyOwner: null, - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - }; - const resolvedValue = { - ...alertToCreate, - id: '123', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.post.mockResolvedValueOnce(resolvedValue); - - const result = await createAlert({ http, alert: alertToCreate }); - expect(result).toEqual(resolvedValue); - expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert", - Object { - "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"notifyWhen\\":\\"onActionGroupChange\\",\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKeyOwner\\":null,\\"createdBy\\":null,\\"updatedBy\\":null,\\"muteAll\\":false,\\"mutedInstanceIds\\":[]}", - }, - ] - `); - }); -}); - -describe('updateAlert', () => { - test('should call alert update API', async () => { - const alertToUpdate = { - throttle: '1m', - consumer: 'alerts', - name: 'test', - tags: ['foo'], - schedule: { - interval: '1m', - }, - params: {}, - actions: [], - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKey: null, - apiKeyOwner: null, - notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, - }; - const resolvedValue: Alert = { - ...alertToUpdate, - id: '123', - enabled: true, - alertTypeId: 'test', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.put.mockResolvedValueOnce(resolvedValue); - - const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); - expect(result).toEqual(resolvedValue); - expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert/123", - Object { - "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[],\\"notifyWhen\\":\\"onThrottleInterval\\"}", - }, - ] - `); - }); -}); - -describe('enableAlert', () => { - test('should call enable alert API', async () => { - const result = await enableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - ] - `); - }); -}); - -describe('disableAlert', () => { - test('should call disable alert API', async () => { - const result = await disableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - ] - `); - }); -}); - -describe('muteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_mute", - ], - ] - `); - }); -}); - -describe('unmuteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_unmute", - ], - ] - `); - }); -}); - -describe('muteAlert', () => { - test('should call mute alert API', async () => { - const result = await muteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlert', () => { - test('should call unmute alert API', async () => { - const result = await unmuteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - ] - `); - }); -}); - -describe('enableAlerts', () => { - test('should call enable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await enableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - Array [ - "/api/alerts/alert/2/_enable", - ], - Array [ - "/api/alerts/alert/3/_enable", - ], - ] - `); - }); -}); - -describe('disableAlerts', () => { - test('should call disable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await disableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - Array [ - "/api/alerts/alert/2/_disable", - ], - Array [ - "/api/alerts/alert/3/_disable", - ], - ] - `); - }); -}); - -describe('muteAlerts', () => { - test('should call mute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await muteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - Array [ - "/api/alerts/alert/2/_mute_all", - ], - Array [ - "/api/alerts/alert/3/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlerts', () => { - test('should call unmute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await unmuteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - Array [ - "/api/alerts/alert/2/_unmute_all", - ], - Array [ - "/api/alerts/alert/3/_unmute_all", - ], - ] - `); - }); -}); - -describe('alertingFrameworkHealth', () => { - test('should call alertingFrameworkHealth API', async () => { - const result = await alertingFrameworkHealth({ http }); - expect(result).toEqual(undefined); - expect(http.get.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/_health", - ], - ] - `); - }); -}); - -describe('mapFiltersToKql', () => { - test('should handle no filters', () => { - expect(mapFiltersToKql({})).toEqual([]); - }); - - test('should handle typesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - }) - ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); - }); - - test('should handle actionTypesFilter', () => { - expect( - mapFiltersToKql({ - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); - }); - - test('should handle typesFilter and actionTypesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - 'alert.attributes.executionStatus.status:(alert or statuses or filter)', - ]); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts deleted file mode 100644 index 80ff415582191..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HttpSetup } from 'kibana/public'; -import { Errors, identity } from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { pick } from 'lodash'; -import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common'; -import { LEGACY_BASE_ALERT_API_PATH } from '../constants'; -import { - Alert, - AlertAggregations, - AlertType, - AlertUpdates, - AlertTaskState, - AlertInstanceSummary, - Pagination, - Sorting, -} from '../../types'; - -export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`); -} - -export async function loadAlert({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}`); -} - -type EmptyHttpResponse = ''; -export async function loadAlertState({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http - .get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/state`) - .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) - .then((state: AlertTaskState) => { - return pipe( - alertStateSchema.decode(state), - fold((e: Errors) => { - throw new Error(`Alert "${alertId}" has invalid state`); - }, identity) - ); - }); -} - -export async function loadAlertInstanceSummary({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/_instance_summary`); -} - -export const mapFiltersToKql = ({ - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): string[] => { - const filters = []; - if (typesFilter && typesFilter.length) { - filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); - } - if (actionTypesFilter && actionTypesFilter.length) { - filters.push( - [ - '(', - actionTypesFilter - .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) - .join(' OR '), - ')', - ].join('') - ); - } - if (alertStatusesFilter && alertStatusesFilter.length) { - filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); - } - return filters; -}; - -export async function loadAlerts({ - http, - page, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, - sort = { field: 'name', direction: 'asc' }, -}: { - http: HttpSetup; - page: Pagination; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; - sort?: Sorting; -}): Promise<{ - page: number; - perPage: number; - total: number; - data: Alert[]; -}> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_find`, { - query: { - page: page.index + 1, - per_page: page.size, - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - sort_field: sort.field, - sort_order: sort.direction, - }, - }); -} - -export async function loadAlertAggregations({ - http, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - http: HttpSetup; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_aggregate`, { - query: { - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - }, - }); -} - -export async function deleteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise<{ successes: string[]; errors: string[] }> { - const successes: string[] = []; - const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`))).then( - function (fulfilled) { - successes.push(...fulfilled); - }, - function (rejected) { - errors.push(...rejected); - } - ); - return { successes, errors }; -} - -export async function createAlert({ - http, - alert, -}: { - http: HttpSetup; - alert: Omit< - AlertUpdates, - 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' - >; -}): Promise { - return await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert`, { - body: JSON.stringify(alert), - }); -} - -export async function updateAlert({ - http, - alert, - id, -}: { - http: HttpSetup; - alert: Pick< - AlertUpdates, - 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' - >; - id: string; -}): Promise { - return await http.put(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`, { - body: JSON.stringify( - pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) - ), - }); -} - -export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_enable`); -} - -export async function enableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => enableAlert({ id, http }))); -} - -export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_disable`); -} - -export async function disableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => disableAlert({ id, http }))); -} - -export async function muteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_mute`); -} - -export async function unmuteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_unmute`); -} - -export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_mute_all`); -} - -export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map((id) => muteAlert({ http, id }))); -} - -export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_unmute_all`); -} - -export async function unmuteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); -} - -export async function alertingFrameworkHealth({ - http, -}: { - http: HttpSetup; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_health`); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts new file mode 100644 index 0000000000000..57feb1e7abae9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertAggregations } from './aggregate'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertAggregations', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call aggregate API with base parameters', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with searchText', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http, searchText: 'apples' }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'foo', + actionTypesFilter: ['action', 'type'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'baz', + actionTypesFilter: ['action', 'type'], + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts new file mode 100644 index 0000000000000..589677ec2322d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AlertAggregations } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_execution_status: alertExecutionStatus, + ...rest +}: any) => ({ + ...rest, + alertExecutionStatus, +}); + +export async function loadAlertAggregations({ + http, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + http: HttpSetup; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): Promise { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { + query: { + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + }, + }); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts new file mode 100644 index 0000000000000..e94da81d0f5d5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertInstanceSummary } from '../../../../../alerting/common'; +import { loadAlertInstanceSummary } from './alert_summary'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertInstanceSummary', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertInstanceSummary = { + instances: {}, + consumer: 'alerts', + enabled: true, + errorMessages: [], + id: 'test', + lastRun: '2021-04-01T22:18:27.609Z', + muteAll: false, + name: 'test', + alertTypeId: '.index-threshold', + status: 'OK', + statusEndDate: '2021-04-01T22:19:25.174Z', + statusStartDate: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }; + + http.get.mockResolvedValueOnce({ + alerts: {}, + consumer: 'alerts', + enabled: true, + error_messages: [], + id: 'test', + last_run: '2021-04-01T22:18:27.609Z', + mute_all: false, + name: 'test', + rule_type_id: '.index-threshold', + status: 'OK', + status_end_date: '2021-04-01T22:19:25.174Z', + status_start_date: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }); + + const result = await loadAlertInstanceSummary({ http, alertId: 'test' }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rule/test/_alert_summary", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts new file mode 100644 index 0000000000000..e37c0640ec1c8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { AlertInstanceSummary } from '../../../types'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + alerts, + rule_type_id: alertTypeId, + mute_all: muteAll, + status_start_date: statusStartDate, + status_end_date: statusEndDate, + error_messages: errorMessages, + last_run: lastRun, + ...rest +}: any) => ({ + ...rest, + alertTypeId, + muteAll, + statusStartDate, + statusEndDate, + errorMessages, + lastRun, + instances: alerts, +}); + +export async function loadAlertInstanceSummary({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/_alert_summary`); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts new file mode 100644 index 0000000000000..749cf53cf740b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertExecutionStatus } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { Alert, AlertAction } from '../../../types'; + +const transformAction: RewriteRequestCase = ({ + group, + id, + connector_type_id: actionTypeId, + params, +}) => ({ + group, + id, + params, + actionTypeId, +}); + +const transformExecutionStatus: RewriteRequestCase = ({ + last_execution_date: lastExecutionDate, + ...rest +}) => ({ + lastExecutionDate, + ...rest, +}); + +export const transformAlert: RewriteRequestCase = ({ + rule_type_id: alertTypeId, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + api_key_owner: apiKeyOwner, + notify_when: notifyWhen, + mute_all: muteAll, + muted_alert_ids: mutedInstanceIds, + scheduled_task_id: scheduledTaskId, + execution_status: executionStatus, + actions: actions, + ...rest +}: any) => ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, + actions: actions + ? actions.map((action: AsApiContract) => transformAction(action)) + : [], + scheduledTaskId, + ...rest, +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts new file mode 100644 index 0000000000000..8d1ec57a4e63e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertUpdates } from '../../../types'; +import { createAlert } from './create'; + +const http = httpServiceMock.createStartContract(); + +describe('createAlert', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call create alert API', async () => { + const resolvedValue = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + rule_type_id: '.index-threshold', + notify_when: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + connector_type_id: '.server-log', + }, + ], + scheduled_task_id: '1', + execution_status: { status: 'pending', last_execution_date: '2021-04-01T21:33:13.250Z' }, + create_at: '2021-04-01T21:33:13.247Z', + updated_at: '2021-04-01T21:33:13.247Z', + }; + const alertToCreate: Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' + > = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + enabled: true, + throttle: null, + alertTypeId: '.index-threshold', + notifyWhen: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '83d4d860-9316-11eb-a145-93ab369a4461', + params: { + level: 'info', + message: + "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}", + }, + actionTypeId: '.server-log', + }, + ], + createdAt: new Date('2021-04-01T21:33:13.247Z'), + updatedAt: new Date('2021-04-01T21:33:13.247Z'), + apiKeyOwner: '', + }; + http.post.mockResolvedValueOnce(resolvedValue); + + const result = await createAlert({ http, alert: alertToCreate }); + expect(result).toEqual({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + }, + ], + alertTypeId: '.index-threshold', + apiKeyOwner: undefined, + consumer: 'alerts', + create_at: '2021-04-01T21:33:13.247Z', + createdAt: undefined, + createdBy: undefined, + executionStatus: { + lastExecutionDate: '2021-04-01T21:33:13.250Z', + status: 'pending', + }, + muteAll: undefined, + mutedInstanceIds: undefined, + name: 'test', + notifyWhen: 'onActionGroupChange', + params: { + aggType: 'count', + groupBy: 'all', + index: ['.kibana'], + termSize: 5, + threshold: [1000], + thresholdComparator: '>', + timeField: 'alert.executionStatus.lastExecutionDate', + timeWindowSize: 5, + timeWindowUnit: 'm', + }, + schedule: { + interval: '1m', + }, + scheduledTaskId: '1', + tags: [], + updatedAt: '2021-04-01T21:33:13.247Z', + updatedBy: undefined, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts new file mode 100644 index 0000000000000..bd92769b4bbf3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { Alert, AlertUpdates } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +type AlertCreateBody = Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + alertTypeId, + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + rule_type_id: alertTypeId, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function createAlert({ + http, + alert, +}: { + http: HttpSetup; + alert: AlertCreateBody; +}): Promise { + const res = await http.post(`${BASE_ALERTING_API_PATH}/rule`, { + body: JSON.stringify(rewriteBodyRequest(alert)), + }); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts new file mode 100644 index 0000000000000..b279e4c0237d9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { deleteAlerts } from './delete'; + +const http = httpServiceMock.createStartContract(); + +describe('deleteAlerts', () => { + test('should call delete API for each alert', async () => { + const ids = ['1', '2', '3']; + const result = await deleteAlerts({ http, ids }); + expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); + expect(http.delete.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1", + ], + Array [ + "/api/alerting/rule/2", + ], + Array [ + "/api/alerting/rule/3", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts new file mode 100644 index 0000000000000..870d5a409c3dd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function deleteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise<{ successes: string[]; errors: string[] }> { + const successes: string[] = []; + const errors: string[] = []; + await Promise.all(ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${id}`))).then( + function (fulfilled) { + successes.push(...fulfilled); + }, + function (rejected) { + errors.push(...rejected); + } + ); + return { successes, errors }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts new file mode 100644 index 0000000000000..90d1cd13096e8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { disableAlert, disableAlerts } from './disable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + ] + `); + }); +}); + +describe('disableAlerts', () => { + test('should call disable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await disableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + Array [ + "/api/alerting/rule/2/_disable", + ], + Array [ + "/api/alerting/rule/3/_disable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts new file mode 100644 index 0000000000000..cc0939fbebfbd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_disable`); +} + +export async function disableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => disableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts new file mode 100644 index 0000000000000..ef65e8b605cba --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { enableAlert, enableAlerts } from './enable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + ] + `); + }); +}); + +describe('enableAlerts', () => { + test('should call enable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await enableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + Array [ + "/api/alerting/rule/2/_enable", + ], + Array [ + "/api/alerting/rule/3/_enable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts new file mode 100644 index 0000000000000..3c16ffaec6223 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_enable`); +} + +export async function enableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => enableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts new file mode 100644 index 0000000000000..f2d8337eb4091 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlert } from './get_rule'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + rule_type_id: '.index-threshold', + created_by: 'elastic', + updated_by: 'elastic', + created_at: '2021-04-01T20:29:18.652Z', + updated_at: '2021-04-01T20:33:38.260Z', + api_key_owner: 'elastic', + notify_when: 'onThrottleInterval', + mute_all: false, + muted_alert_ids: [], + scheduled_task_id: '1', + execution_status: { status: 'ok', last_execution_date: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + connector_type_id: '.index', + }, + ], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual({ + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + alertTypeId: '.index-threshold', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-04-01T20:29:18.652Z', + updatedAt: '2021-04-01T20:33:38.260Z', + apiKeyOwner: 'elastic', + notifyWhen: 'onThrottleInterval', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '1', + executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + actionTypeId: '.index', + }, + ], + }); + expect(http.get).toHaveBeenCalledWith(`/api/alerting/rule/${alertId}`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts new file mode 100644 index 0000000000000..2e4cbc9b50c51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { Alert } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +export async function loadAlert({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule/${alertId}`); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts new file mode 100644 index 0000000000000..e08306bee0f9c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { alertingFrameworkHealth } from './health'; + +describe('alertingFrameworkHealth', () => { + const http = httpServiceMock.createStartContract(); + test('should call alertingFrameworkHealth API', async () => { + http.get.mockResolvedValueOnce({ + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + }); + const result = await alertingFrameworkHealth({ http }); + expect(result).toEqual({ + alertingFrameworkHeath: { + decryptionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + executionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + readHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }); + expect(http.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/_health", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts new file mode 100644 index 0000000000000..9468f4b3c03e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { AlertingFrameworkHealth, AlertsHealth } from '../../../../../alerting/common'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +const rewriteAlertingFrameworkHeath: RewriteRequestCase = ({ + decryption_health: decryptionHealth, + execution_health: executionHealth, + read_health: readHealth, + ...res +}: AsApiContract) => ({ + decryptionHealth, + executionHealth, + readHealth, + ...res, +}); + +const rewriteBodyRes: RewriteRequestCase = ({ + is_sufficiently_secure: isSufficientlySecure, + has_permanent_encryption_key: hasPermanentEncryptionKey, + alerting_framework_heath: alertingFrameworkHeath, + ...res +}: AsApiContract) => ({ + isSufficientlySecure, + hasPermanentEncryptionKey, + alertingFrameworkHeath, + ...res, +}); + +export async function alertingFrameworkHealth({ + http, +}: { + http: HttpSetup; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/_health`); + const alertingFrameworkHeath = rewriteAlertingFrameworkHeath(res.alerting_framework_heath); + return { ...rewriteBodyRes(res), alertingFrameworkHeath }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts new file mode 100644 index 0000000000000..a0b090a474e28 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { alertingFrameworkHealth } from './health'; +export { mapFiltersToKql } from './map_filters_to_kql'; +export { loadAlertAggregations } from './aggregate'; +export { createAlert } from './create'; +export { deleteAlerts } from './delete'; +export { disableAlert, disableAlerts } from './disable'; +export { enableAlert, enableAlerts } from './enable'; +export { loadAlert } from './get_rule'; +export { loadAlertInstanceSummary } from './alert_summary'; +export { muteAlertInstance } from './mute_alert'; +export { muteAlert, muteAlerts } from './mute'; +export { loadAlertTypes } from './rule_types'; +export { loadAlerts } from './rules'; +export { loadAlertState } from './state'; +export { unmuteAlertInstance } from './unmute_alert'; +export { unmuteAlert, unmuteAlerts } from './unmute'; +export { updateAlert } from './update'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts new file mode 100644 index 0000000000000..4e5e2a412dad6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapFiltersToKql } from './map_filters_to_kql'; + +describe('mapFiltersToKql', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should handle no filters', () => { + expect(mapFiltersToKql({})).toEqual([]); + }); + + test('should handle typesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + }) + ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); + }); + + test('should handle actionTypesFilter', () => { + expect( + mapFiltersToKql({ + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); + }); + + test('should handle typesFilter and actionTypesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + ]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts new file mode 100644 index 0000000000000..4c30e960034bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mapFiltersToKql = ({ + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): string[] => { + const filters = []; + if (typesFilter && typesFilter.length) { + filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); + } + if (actionTypesFilter && actionTypesFilter.length) { + filters.push( + [ + '(', + actionTypesFilter + .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) + .join(' OR '), + ')', + ].join('') + ); + } + if (alertStatusesFilter && alertStatusesFilter.length) { + filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); + } + return filters; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts new file mode 100644 index 0000000000000..75143dd6b7f85 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlert, muteAlerts } from './mute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + ] + `); + }); +}); + +describe('muteAlerts', () => { + test('should call mute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await muteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + Array [ + "/api/alerting/rule/2/_mute_all", + ], + Array [ + "/api/alerting/rule/3/_mute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts new file mode 100644 index 0000000000000..22a96d7a11ff3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_mute_all`); +} + +export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { + await Promise.all(ids.map((id) => muteAlert({ http, id }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts new file mode 100644 index 0000000000000..4365cce42c8c3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlertInstance } from './mute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('muteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_mute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts new file mode 100644 index 0000000000000..0bb05010cfa3c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_mute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts new file mode 100644 index 0000000000000..71513ed0c6e61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertType } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertTypes } from './rule_types'; +import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertTypes', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertType[] = [ + { + id: 'test', + name: 'Test', + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], + }, + producer: ALERTS_FEATURE_ID, + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }, + ]; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertTypes({ http }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule_types", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts new file mode 100644 index 0000000000000..54369d7959c93 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AlertType } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteResponseRes = (results: Array>): AlertType[] => { + return results.map((item) => rewriteBodyReq(item)); +}; + +const rewriteBodyReq: RewriteRequestCase = ({ + enabled_in_license: enabledInLicense, + recovery_action_group: recoveryActionGroup, + action_groups: actionGroups, + default_action_group_id: defaultActionGroupId, + minimum_license_required: minimumLicenseRequired, + action_variables: actionVariables, + authorized_consumers: authorizedConsumers, + ...rest +}: AsApiContract) => ({ + enabledInLicense, + recoveryActionGroup, + actionGroups, + defaultActionGroupId, + minimumLicenseRequired, + actionVariables, + authorizedConsumers, + ...rest, +}); + +export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule_types`); + return rewriteResponseRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts new file mode 100644 index 0000000000000..602507c08066c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlerts } from './rules'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlerts', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call find API with base parameters', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'foo', + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText and tagsFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'apples, foo, baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "apples, foo, baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts new file mode 100644 index 0000000000000..f0bbb57180bb4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, Pagination, Sorting } from '../../../types'; +import { AsApiContract } from '../../../../../actions/common'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { transformAlert } from './common_transformations'; + +const rewriteResponseRes = (results: Array>): Alert[] => { + return results.map((item) => transformAlert(item)); +}; + +export async function loadAlerts({ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, + sort = { field: 'name', direction: 'asc' }, +}: { + http: HttpSetup; + page: Pagination; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; + sort?: Sorting; +}): Promise<{ + page: number; + perPage: number; + total: number; + data: Alert[]; +}> { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${BASE_ALERTING_API_PATH}/rules/_find`, { + query: { + page: page.index + 1, + per_page: page.size, + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + sort_field: sort.field, + sort_order: sort.direction, + }, + }); + return { + page: res.page, + perPage: res.per_page, + total: res.total, + data: rewriteResponseRes(res.data), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts new file mode 100644 index 0000000000000..ae27352be0b90 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertState } from './state'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertState', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: {}, + second_instance: {}, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: {}, + second_instance: {}, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should parse AlertInstances', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual({ + ...resolvedValue, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date('2020-02-09T23:15:41.941Z'), + }, + }, + }, + }, + }); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should handle empty response from api', async () => { + const alertId = uuid.v4(); + http.get.mockResolvedValueOnce(''); + + expect(await loadAlertState({ http, alertId })).toEqual({}); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts new file mode 100644 index 0000000000000..428bc5b99a70b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { Errors, identity } from 'io-ts'; +import { AlertTaskState } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { alertStateSchema } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_type_state: alertTypeState, + alerts: alertInstances, + previous_started_at: previousStartedAt, + ...rest +}: any) => ({ + ...rest, + alertTypeState, + alertInstances, + previousStartedAt, +}); + +type EmptyHttpResponse = ''; +export async function loadAlertState({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http + .get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/state`) + .then((state: AsApiContract | EmptyHttpResponse) => + state ? rewriteBodyRes(state) : {} + ) + .then((state: AlertTaskState) => { + return pipe( + alertStateSchema.decode(state), + fold((e: Errors) => { + throw new Error(`Alert "${alertId}" has invalid state`); + }, identity) + ); + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts new file mode 100644 index 0000000000000..68a6feeb65e1e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlert, unmuteAlerts } from './unmute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('unmuteAlerts', () => { + test('should call unmute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await unmuteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + Array [ + "/api/alerting/rule/2/_unmute_all", + ], + Array [ + "/api/alerting/rule/3/_unmute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts new file mode 100644 index 0000000000000..c65be6a670a89 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_unmute_all`); +} + +export async function unmuteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts new file mode 100644 index 0000000000000..c0131cbab0ebf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlertInstance } from './unmute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('unmuteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_unmute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts new file mode 100644 index 0000000000000..60d2cca72b85e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_unmute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts new file mode 100644 index 0000000000000..745a94b8d1134 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Alert } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { updateAlert } from './update'; +import { AlertNotifyWhenType } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('updateAlert', () => { + test('should call alert update API', async () => { + const alertToUpdate = { + throttle: '1m', + consumer: 'alerts', + name: 'test', + tags: ['foo'], + schedule: { + interval: '1m', + }, + params: {}, + actions: [], + createdAt: new Date('1970-01-01T00:00:00.000Z'), + updatedAt: new Date('1970-01-01T00:00:00.000Z'), + apiKey: null, + apiKeyOwner: null, + notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, + }; + const resolvedValue: Alert = { + ...alertToUpdate, + id: '123', + enabled: true, + alertTypeId: 'test', + createdBy: null, + updatedBy: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + http.put.mockResolvedValueOnce(resolvedValue); + + const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); + expect(result).toEqual(resolvedValue); + expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule/123", + Object { + "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts new file mode 100644 index 0000000000000..44b9306949f81 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { pick } from 'lodash'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, AlertUpdates } from '../../../types'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { transformAlert } from './common_transformations'; + +type AlertUpdatesBody = Pick< + AlertUpdates, + 'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function updateAlert({ + http, + alert, + id, +}: { + http: HttpSetup; + alert: Pick< + AlertUpdates, + 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' + >; + id: string; +}): Promise { + const res = await http.put(`${BASE_ALERTING_API_PATH}/rule/${id}`, { + body: JSON.stringify( + rewriteBodyRequest( + pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) + ) + ), + }); + return transformAlert(res); +} From df46dc19004d5529f04dfd08f07ad389862b0f4b Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 08:05:11 -0700 Subject: [PATCH 065/131] skip flaky suite (#91107) --- .../migrationsv2/integration_tests/migration.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index fd62fd107648e..4d41a147bc0ef 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -19,7 +19,8 @@ import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -describe('migration v2', () => { +// FLAKY: https://github.com/elastic/kibana/issues/91107 +describe.skip('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; From c89922a55ca8da6c739f351fda04e34e7b74c16f Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 7 Apr 2021 11:08:12 -0400 Subject: [PATCH 066/131] [project-assigner] remove extra bracket in issue-mappings config (#96428) --- .github/workflows/project-assigner.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 37d04abda7530..4966a0b506317 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,5 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}], {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} - - From 5de5f23fc3160a264e1cedfb8a43d2fc589b275c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 7 Apr 2021 10:16:39 -0500 Subject: [PATCH 067/131] Updated asset code for canvas-expression-lifecycle (#96414) The demo code need the asset to be wrapped in "' and asset- to be appended onto the id. Co-authored-by: Zachary E Baxter --- docs/canvas/canvas-expression-lifecycle.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc index 7d48c593f9e18..17903408dff0e 100644 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ b/docs/canvas/canvas-expression-lifecycle.asciidoc @@ -177,8 +177,8 @@ Since all of the sub-expressions are now resolved into actual values, the < Date: Wed, 7 Apr 2021 10:19:29 -0500 Subject: [PATCH 068/131] Adds canvas `clog` function (#96418) * Add canvas `clog` function in the doc * Add basic example to the `clog` canvcas function * clog canvas function: switch definition/purpose Co-authored-by: Laurent HUET --- .../canvas/canvas-function-reference.asciidoc | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index eaaf68eb06195..67210c9d77057 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -376,6 +376,37 @@ Clears the _context_, and returns `null`. *Returns:* `null` +[float] +[[clog_fn]] +=== `clog` + +It outputs the _context_ in the console. This function is for debug purpose. + +*Expression syntax* +[source,js] +---- +clog +---- + +*Code example* +[source,text] +---- +filters + | demodata + | clog + | filterrows fn={getCell "age" | gt 70} + | clog + | pointseries x="time" y="mean(price)" + | plot defaultStyle={seriesStyle lines=1 fill=1} + | render +---- +This prints the `datatable` objects in the browser console before and after the `filterrows` function. + +*Accepts:* `any` + +*Returns:* `any` + + [float] [[columns_fn]] === `columns` From 532145b4188f7c93315f9fcd2c4e3c1487757e4f Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 7 Apr 2021 10:26:34 -0500 Subject: [PATCH 069/131] [DOCS] Add s an example for Timelion yaxis function (#96429) --- docs/user/dashboard/timelion.asciidoc | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 80ce77f30c75e..ff71cd7b383bd 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -33,6 +33,40 @@ If the value of your parameter contains spaces or commas you have to put the val .es(q='some query', index=logstash-*) +[float] +[[customize-data-series-y-axis]] +===== .yaxis() function + +{kib} supports many y-axis scales and ranges for your data series. + +The `.yaxis()` function supports the following parameters: + +* *yaxis* — The numbered y-axis to plot the series on. For example, use `.yaxis(2)` to display a second y-axis. +* *min* — The minimum value for the y-axis range. +* *max* — The maximum value for the y-axis range. +* *position* — The location of the units. Values include `left` or `right`. +* *label* — The label for the axis. +* *color* — The color of the axis label. +* *units* — The function to use for formatting the y-axis labels. Values include `bits`, `bits/s`, `bytes`, `bytes/s`, `currency(:ISO 4217 currency code)`, `percent`, and `custom(:prefix:suffix)`. +* *tickDecimals* — The tick decimal precision. + +Example: + +[source,text] +---------------------------------- +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric='avg:bytes') + .label('Average Bytes for request') + .title('Memory consumption over time in bytes').yaxis(1,units=bytes,position=left), <1> +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric=avg:machine.ram) + .label('Average Machine RAM amount').yaxis(2,units=bytes,position=right) <2> +---------------------------------- + +<1> `.yaxis(1,units=bytes,position=left)` — Specifies the first y-axis for the first data series, and changes the units on the left. +<2> `.yaxis(2,units=bytes,position=left)` — Specifies the second y-axis for the second data series, and changes the units on the right. [float] ==== Tutorial: Create visualizations with Timelion From 818a74003309d1e731cef71e76ab71caffbf89ed Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 7 Apr 2021 11:56:31 -0400 Subject: [PATCH 070/131] [App Search] Added a query performance rating to the Result Settings page (#96230) --- .../query_performance/index.ts | 8 ++ .../query_performance.test.tsx | 59 +++++++++++++ .../query_performance/query_performance.tsx | 87 +++++++++++++++++++ .../result_settings/result_settings.test.tsx | 19 +++- .../result_settings/result_settings.tsx | 7 +- .../result_settings_logic.test.ts | 71 +++++++++++++++ .../result_settings/result_settings_logic.ts | 32 ++++++- .../sample_response/sample_response.tsx | 3 +- 8 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts new file mode 100644 index 0000000000000..0bd18ea640850 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { QueryPerformance } from './query_performance'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx new file mode 100644 index 0000000000000..0c62b783a47ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; + +import { QueryPerformance } from './query_performance'; + +describe('QueryPerformance', () => { + const values = { + queryPerformanceScore: 1, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders as green with the text "optimal" for a performance score of less than 6', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#59deb4'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: optimal'); + }); + + it('renders as blue with the text "good" for a performance score of less than 11', () => { + setMockValues({ + queryPerformanceScore: 10, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#40bfff'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: good'); + }); + + it('renders as yellow with the text "standard" for a performance score of less than 21', () => { + setMockValues({ + queryPerformanceScore: 20, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#fed566'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: standard'); + }); + + it('renders as red with the text "delayed" for a performance score of 21 or more', () => { + setMockValues({ + queryPerformanceScore: 100, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#ff9173'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: delayed'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx new file mode 100644 index 0000000000000..e3dfddc35d88c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +enum QueryPerformanceRating { + Optimal = 'Optimal', + Good = 'Good', + Standard = 'Standard', + Delayed = 'Delayed', +} + +const QUERY_PERFORMANCE_LABEL = (performanceValue: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformanceLabel', { + defaultMessage: 'Query performance: {performanceValue}', + values: { + performanceValue, + }, + }); + +const QUERY_PERFORMANCE_OPTIMAL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.optimalValue', + { defaultMessage: 'optimal' } +); + +const QUERY_PERFORMANCE_GOOD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.goodValue', + { defaultMessage: 'good' } +); + +const QUERY_PERFORMANCE_STANDARD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.standardValue', + { defaultMessage: 'standard' } +); + +const QUERY_PERFORMANCE_DELAYED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.delayedValue', + { defaultMessage: 'delayed' } +); + +const badgeText: Record = { + [QueryPerformanceRating.Optimal]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_OPTIMAL), + [QueryPerformanceRating.Good]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_GOOD), + [QueryPerformanceRating.Standard]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_STANDARD), + [QueryPerformanceRating.Delayed]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_DELAYED), +}; + +const badgeColors: Record = { + [QueryPerformanceRating.Optimal]: '#59deb4', + [QueryPerformanceRating.Good]: '#40bfff', + [QueryPerformanceRating.Standard]: '#fed566', + [QueryPerformanceRating.Delayed]: '#ff9173', +}; + +const getPerformanceRating = (score: number) => { + switch (true) { + case score < 6: + return QueryPerformanceRating.Optimal; + case score < 11: + return QueryPerformanceRating.Good; + case score < 21: + return QueryPerformanceRating.Standard; + default: + return QueryPerformanceRating.Delayed; + } +}; + +export const QueryPerformance: React.FC = () => { + const { queryPerformanceScore } = useValues(ResultSettingsLogic); + const performanceRating = getPerformanceRating(queryPerformanceScore); + return ( + + {badgeText[performanceRating]} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 3388894c230a0..9eda1362e04fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -7,7 +7,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockActions } from '../../../__mocks__'; +import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; @@ -15,12 +15,19 @@ import { shallow } from 'enzyme'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; describe('RelevanceTuning', () => { + const values = { + dataLoading: false, + }; + const actions = { initializeResultSettingsData: jest.fn(), }; + beforeEach(() => { + setMockValues(values); setMockActions(actions); jest.clearAllMocks(); }); @@ -28,10 +35,20 @@ describe('RelevanceTuning', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); + expect(wrapper.find(SampleResponse).exists()).toBe(true); }); it('initializes result settings data when mounted', () => { shallow(); expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); + + it('renders a loading screen if data has not loaded yet', () => { + setMockValues({ + dataLoading: true, + }); + const wrapper = shallow(); + expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); + expect(wrapper.find(SampleResponse).exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 7f4373835f8d5..336f3f663119f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -7,13 +7,15 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; + import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; @@ -26,12 +28,15 @@ interface Props { } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { + const { dataLoading } = useValues(ResultSettingsLogic); const { initializeResultSettingsData } = useActions(ResultSettingsLogic); useEffect(() => { initializeResultSettingsData(); }, []); + if (dataLoading) return ; + return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index e7bb065b596c3..a9c161b2bb5be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -40,6 +40,7 @@ describe('ResultSettingsLogic', () => { stagedUpdates: false, nonTextResultFields: {}, textResultFields: {}, + queryPerformanceScore: 0, }; // Values without selectors @@ -487,6 +488,76 @@ describe('ResultSettingsLogic', () => { }); }); }); + + describe('queryPerformanceScore', () => { + describe('returns a score for the current query performance based on the result settings', () => { + it('considers a text value with raw set (but no size) as worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size over 250 as also worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 251 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size less than or equal to 250 as worth 1', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 250 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1); + }); + + it('considers a text value with a snippet set as worth 2', () => { + mount({ + resultFields: { foo: { snippet: true, snippetSize: 50, snippetFallback: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(2); + }); + + it('will sum raw and snippet values if both are set', () => { + mount({ + resultFields: { foo: { snippet: true, raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + // 1.5 (raw) + 2 (snippet) = 3.5 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(3.5); + }); + + it('considers a non-text value with raw set as 0.2', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'number' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(0.2); + }); + + it('can sum variations of all the prior', () => { + mount({ + resultFields: { + foo: { raw: true }, + bar: { raw: true, snippet: true }, + baz: { raw: true }, + }, + schema: { + foo: 'text' as SchemaTypes, + bar: 'text' as SchemaTypes, + baz: 'number' as SchemaTypes, + }, + }); + // 1.5 (foo) + 3.5 (bar) + baz (.2) = 5.2 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(5.2); + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index 22f4c44f8b543..c345ae7e02e8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -71,18 +71,19 @@ interface ResultSettingsValues { dataLoading: boolean; saving: boolean; openModal: OpenModal; - nonTextResultFields: FieldResultSettingObject; - textResultFields: FieldResultSettingObject; resultFields: FieldResultSettingObject; - serverResultFields: ServerFieldResultSettingObject; lastSavedResultFields: FieldResultSettingObject; schema: Schema; schemaConflicts: SchemaConflicts; // Selectors + textResultFields: FieldResultSettingObject; + nonTextResultFields: FieldResultSettingObject; + serverResultFields: ServerFieldResultSettingObject; resultFieldsAtDefaultSettings: boolean; resultFieldsEmpty: boolean; stagedUpdates: true; reducedServerResultFields: ServerFieldResultSettingObject; + queryPerformanceScore: number; } export const ResultSettingsLogic = kea>({ @@ -221,6 +222,31 @@ export const ResultSettingsLogic = kea [selectors.serverResultFields, selectors.schema], + (serverResultFields: ServerFieldResultSettingObject, schema: Schema) => { + return Object.entries(serverResultFields).reduce((acc, [fieldName, resultField]) => { + let newAcc = acc; + if (resultField.raw) { + if (schema[fieldName] !== 'text') { + newAcc += 0.2; + } else if ( + typeof resultField.raw === 'object' && + resultField.raw.size && + resultField.raw.size <= 250 + ) { + newAcc += 1.0; + } else { + newAcc += 1.5; + } + } + if (resultField.snippet) { + newAcc += 2.0; + } + return newAcc; + }, 0); + }, + ], }), listeners: ({ actions, values }) => ({ clearRawSizeForField: ({ fieldName }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx index ae91b9648356c..2d0cced3730ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { QueryPerformance } from '../query_performance'; import { ResultSettingsLogic } from '../result_settings_logic'; import { SampleResponseLogic } from './sample_response_logic'; @@ -48,7 +49,7 @@ export const SampleResponse: React.FC = () => { - {/* TODO */} + From b96f60f72740f46f600b2a557bdebd9b603e08fd Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 18:11:42 +0200 Subject: [PATCH 071/131] Bump postcss-svgo from 4.0.2 to 4.0.3 (#96409) --- yarn.lock | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4fba8dc85a09e..0390c2f7cdaf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15892,11 +15892,6 @@ hsla-regex@^1.0.0: resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= -html-comment-regex@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" - integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== - html-element-map@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" @@ -17133,13 +17128,6 @@ is-subset@^0.1.1: resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -22670,11 +22658,10 @@ postcss-selector-parser@^6.0.4: util-deprecate "^1.0.2" postcss-svgo@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" - integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== dependencies: - is-svg "^3.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" svgo "^1.0.0" From ac46802830ceef3f6ce830a5fa68841a95e08102 Mon Sep 17 00:00:00 2001 From: hardikpnsp Date: Wed, 7 Apr 2021 21:46:39 +0530 Subject: [PATCH 072/131] [Telemetry] enforce import export type (#96199) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-analytics/tsconfig.json | 1 + packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +++- packages/kbn-telemetry-tools/tsconfig.json | 3 ++- src/plugins/kibana_usage_collection/tsconfig.json | 3 ++- .../telemetry/common/telemetry_config/index.ts | 6 ++---- src/plugins/telemetry/public/index.ts | 2 +- src/plugins/telemetry/server/index.ts | 9 ++++++--- .../telemetry_collection/get_data_telemetry/index.ts | 9 ++------- .../telemetry/server/telemetry_collection/index.ts | 11 ++++------- .../telemetry/server/telemetry_repository/index.ts | 2 +- src/plugins/telemetry/tsconfig.json | 3 ++- .../telemetry_collection_manager/server/index.ts | 2 +- .../telemetry_collection_manager/tsconfig.json | 3 ++- .../telemetry_management_section/public/index.ts | 2 +- .../telemetry_management_section/tsconfig.json | 3 ++- src/plugins/usage_collection/public/index.ts | 2 +- .../usage_collection/server/collector/index.ts | 10 ++++++---- src/plugins/usage_collection/server/index.ts | 7 +++---- src/plugins/usage_collection/tsconfig.json | 3 ++- .../telemetry_collection_xpack/server/index.ts | 2 +- .../server/telemetry_collection/index.ts | 2 +- .../plugins/telemetry_collection_xpack/tsconfig.json | 3 ++- 22 files changed, 48 insertions(+), 44 deletions(-) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index c2e579e7fdbea..80a2255d71805 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,6 +7,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, + "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index 5d946b73d9759..f55a9aa80d40d 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,7 +7,9 @@ */ export { ErrorReporter } from './error_reporter'; -export { TaskContext, createTaskContext } from './task_context'; + +export type { TaskContext } from './task_context'; +export { createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 39946fe9907e5..419af1d02f83b 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,7 +6,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", + "isolatedModules": true }, "include": [ "src/**/*", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index d664d936f6667..ee07dfe589e4a 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/*", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index 84b6486f35b24..cc4ff102742d7 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,7 +9,5 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { - getTelemetryFailureDetails, - TelemetryFailureDetails, -} from './get_telemetry_failure_details'; +export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; +export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 6cca9bdf881dd..47ba7828eaec2 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index debdf7515cd58..1c335426ffd03 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,9 +34,12 @@ export { constants }; export { getClusterUuids, getLocalStats, - TelemetryLocalStats, DATA_TELEMETRY_ID, + buildDataTelemetryPayload, +} from './telemetry_collection'; + +export type { + TelemetryLocalStats, DataTelemetryIndex, DataTelemetryPayload, - buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index def1131dfb1a3..c93b7e872924b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,10 +7,5 @@ */ export { DATA_TELEMETRY_ID } from './constants'; - -export { - getDataTelemetry, - buildDataTelemetryPayload, - DataTelemetryPayload, - DataTelemetryIndex, -} from './get_data_telemetry'; +export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 55f9c7f0e624c..151e89a11a192 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -export { - DATA_TELEMETRY_ID, - DataTelemetryIndex, - DataTelemetryPayload, - buildDataTelemetryPayload, -} from './get_data_telemetry'; -export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; +export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; +export { getLocalStats } from './get_local_stats'; +export type { TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 4e3f046f7611f..594b53259a65f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export { +export type { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index bdced01d9eb6f..6629e479906c9 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 77077b73cf8ad..c0cd124a132c0 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export { +export type { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1bba81769f0dd..1329979860603 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index 28b04418f512d..db6ea17556ed3 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export { TelemetryManagementSectionPluginSetup } from './plugin'; +export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 48e40814b8570..2daee868ac200 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index b9e0e0a8985b1..9b009b1d9e264 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 5f48f9fb93813..d5e0d95659e58 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -export { CollectorSet, CollectorSetPublic } from './collector_set'; -export { - Collector, +export { CollectorSet } from './collector_set'; +export type { CollectorSetPublic } from './collector_set'; +export { Collector } from './collector'; +export type { AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -16,4 +17,5 @@ export { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector, UsageCollectorOptions } from './usage_collector'; +export { UsageCollector } from './usage_collector'; +export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dfc9d19b69646..dd9e6644a827d 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,17 +9,16 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { +export { Collector } from './collector'; +export type { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, - Collector, CollectorFetchContext, } from './collector'; - -export { UsageCollectionSetup } from './plugin'; +export type { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 96b2c4d37e17c..68a0853994e80 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index d924882e17fbd..aab1bdb58fe59 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export { ESLicense } from './telemetry_collection'; +export type { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 4599b068b9b38..c1a11caf44f24 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ESLicense } from './get_license'; +export type { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 476f5926f757a..1221200c7548c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/**/*", From 752308f6d838480a935ed0c55a7467dd5c8145d0 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 09:16:37 -0700 Subject: [PATCH 073/131] skip flaky suite (#96372) --- x-pack/test/accessibility/apps/login_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 02d817612671c..f46a684194810 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'security']); - describe('Security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/96372 + describe.skip('Security', () => { describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); From e6ef368cfeb0fe3965c5d81db5be4633f27a7f91 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 18:35:36 +0200 Subject: [PATCH 074/131] Correctly specify css-minimizer-webpack-plugin as a dev-dependency (#96417) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb383e986e721..d5d136305bc9c 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,6 @@ "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", - "css-minimizer-webpack-plugin": "^1.3.0", "custom-event-polyfill": "^0.3.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -682,6 +681,7 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", + "css-minimizer-webpack-plugin": "^1.3.0", "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", From 9a0c73e515f0e0c7ff88d6b06df46aa34cd8d84f Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 7 Apr 2021 12:36:28 -0400 Subject: [PATCH 075/131] [Security Solution][Endpoint] Endpoint Event Filtering List, Test Data Generator and Loader (#96263) * Added new const to List plugin for new Endpont Event Filter list * Data Generator for event filters ++ script to load event filters (WIP) * refactor `generate_data` to use `BaseDataGenerator` class --- x-pack/plugins/lists/common/constants.ts | 9 ++ .../create_endoint_event_filters_list.ts | 79 +++++++++++++ .../data_generators/base_data_generator.ts | 94 +++++++++++++++ .../data_generators/event_filter_generator.ts | 30 +++++ .../common/endpoint/generate_data.ts | 56 ++------- .../scripts/endpoint/event_filters/index.ts | 111 ++++++++++++++++++ .../scripts/endpoint/load_event_filters.js | 11 ++ 7 files changed, 341 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts create mode 100755 x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index 92d8b6f5f7571..4f897c83cb41d 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -60,3 +60,12 @@ export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps L /** Description of trusted apps agnostic list */ export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; + +/** ID of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_ID = 'endpoint_event_filters'; + +/** Name of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters List'; + +/** Description of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts new file mode 100644 index 0000000000000..95e9df03400af --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import uuid from 'uuid'; + +import { + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, +} from '../../../common/constants'; +import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas'; + +import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; + +interface CreateEndpointEventFiltersListOptions { + savedObjectsClient: SavedObjectsClientContract; + user: string; + tieBreaker?: string; + version: Version; +} + +/** + * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist + * + * @param savedObjectsClient + * @param user + * @param tieBreaker + * @param version + */ +export const createEndpointEventFiltersList = async ({ + savedObjectsClient, + user, + tieBreaker, + version, +}: CreateEndpointEventFiltersListOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); + const dateNow = new Date().toISOString(); + try { + const savedObject = await savedObjectsClient.create( + savedObjectType, + { + comments: undefined, + created_at: dateNow, + created_by: user, + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + entries: undefined, + immutable: false, + item_id: undefined, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + list_type: 'list', + meta: undefined, + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + os_types: [], + tags: [], + tie_breaker_id: tieBreaker ?? uuid.v4(), + type: 'endpoint', + updated_by: user, + version, + }, + { + // We intentionally hard coding the id so that there can only be one Event Filters list within the space + id: ENDPOINT_EVENT_FILTERS_LIST_ID, + } + ); + + return transformSavedObjectToExceptionList({ savedObject }); + } catch (err) { + if (savedObjectsClient.errors.isConflictError(err)) { + return null; + } else { + throw err; + } + } +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts new file mode 100644 index 0000000000000..c0888a6c2a4bd --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import seedrandom from 'seedrandom'; +import uuid from 'uuid'; + +const OS_FAMILY = ['windows', 'macos', 'linux']; + +/** + * A generic base class to assist in creating domain specific data generators. It includes + * several general purpose random data generators for use within the class and exposes one + * public method named `generate()` which should be implemented by sub-classes. + */ +export class BaseDataGenerator { + protected random: seedrandom.prng; + + constructor(seed: string | seedrandom.prng = Math.random().toString()) { + if (typeof seed === 'string') { + this.random = seedrandom(seed); + } else { + this.random = seed; + } + } + + /** + * Generate a new record + */ + public generate(): GeneratedDoc { + throw new Error('method not implemented!'); + } + + /** generate random OS family value */ + protected randomOSFamily(): string { + return this.randomChoice(OS_FAMILY); + } + + /** generate a UUID (v4) */ + protected randomUUID(): string { + return uuid.v4(); + } + + /** Generate a random number up to the max provided */ + protected randomN(max: number): number { + return Math.floor(this.random() * max); + } + + protected *randomNGenerator(max: number, count: number) { + let iCount = count; + while (iCount > 0) { + yield this.randomN(max); + iCount = iCount - 1; + } + } + + /** + * Create an array of a given size and fill it with data provided by a generator + * + * @param lengthLimit + * @param generator + * @protected + */ + protected randomArray(lengthLimit: number, generator: () => T): T[] { + const rand = this.randomN(lengthLimit) + 1; + return [...Array(rand).keys()].map(generator); + } + + protected randomMac(): string { + return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); + } + + protected randomIP(): string { + return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); + } + + protected randomVersion(): string { + return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); + } + + protected randomChoice(choices: T[]): T { + return choices[this.randomN(choices.length)]; + } + + protected randomString(length: number): string { + return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); + } + + protected randomHostname(): string { + return `Host-${this.randomString(10)}`; + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts new file mode 100644 index 0000000000000..6bdbb9cde2034 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseDataGenerator } from './base_data_generator'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../lists/common/constants'; +import { CreateExceptionListItemSchema } from '../../../../lists/common'; +import { getCreateExceptionListItemSchemaMock } from '../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; + +export class EventFilterGenerator extends BaseDataGenerator { + generate(): CreateExceptionListItemSchema { + const overrides: Partial = { + name: `generator event ${this.randomString(5)}`, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + item_id: `generator_endpoint_event_filter_${this.randomUUID()}`, + os_types: [this.randomOSFamily()] as CreateExceptionListItemSchema['os_types'], + tags: ['policy:all'], + namespace_type: 'agnostic', + meta: undefined, + }; + + return Object.assign>( + getCreateExceptionListItemSchemaMock(), + overrides + ); + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 8aec9768dd50d..36d0b0cbf3b21 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -35,6 +35,7 @@ import { EsAssetReference, KibanaAssetReference } from '../../../fleet/common/ty import { agentPolicyStatuses } from '../../../fleet/common/constants'; import { firstNonNullValue } from './models/ecs_safety_helpers'; import { EventOptions } from './types/generator'; +import { BaseDataGenerator } from './data_generators/base_data_generator'; export type Event = AlertEvent | SafeEndpointEvent; /** @@ -386,9 +387,8 @@ const alertsDefaultDataStream = { namespace: 'default', }; -export class EndpointDocGenerator { +export class EndpointDocGenerator extends BaseDataGenerator { commonInfo: HostInfo; - random: seedrandom.prng; sequence: number = 0; /** * The EndpointDocGenerator parameters @@ -396,12 +396,7 @@ export class EndpointDocGenerator { * @param seed either a string to seed the random number generator or a random number generator function */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { - if (typeof seed === 'string') { - this.random = seedrandom(seed); - } else { - this.random = seed; - } - + super(seed); this.commonInfo = this.createHostData(); } @@ -1568,47 +1563,6 @@ export class EndpointDocGenerator { }; } - private randomN(n: number): number { - return Math.floor(this.random() * n); - } - - private *randomNGenerator(max: number, count: number) { - let iCount = count; - while (iCount > 0) { - yield this.randomN(max); - iCount = iCount - 1; - } - } - - private randomArray(lengthLimit: number, generator: () => T): T[] { - const rand = this.randomN(lengthLimit) + 1; - return [...Array(rand).keys()].map(generator); - } - - private randomMac(): string { - return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); - } - - public randomIP(): string { - return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); - } - - private randomVersion(): string { - return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); - } - - private randomChoice(choices: T[]): T { - return choices[this.randomN(choices.length)]; - } - - private randomString(length: number): string { - return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); - } - - private randomHostname(): string { - return `Host-${this.randomString(10)}`; - } - private seededUUIDv4(): string { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } @@ -1646,6 +1600,10 @@ export class EndpointDocGenerator { private randomProcessName(): string { return this.randomChoice(fakeProcessNames); } + + public randomIP(): string { + return super.randomIP(); + } } const fakeProcessNames = [ diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts new file mode 100644 index 0000000000000..93af1f406300c --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { run, RunFn, createFailError } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; +import { AxiosError } from 'axios'; +import bluebird from 'bluebird'; +import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; +import { + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../lists/common/constants'; +import { CreateExceptionListSchema } from '../../../../lists/common'; + +export const cli = () => { + run( + async (options) => { + try { + await createEventFilters(options); + options.log.success(`${options.flags.count} endpoint event filters created`); + } catch (e) { + options.log.error(e); + throw createFailError(e.message); + } + }, + { + description: 'Load Endpoint Event Filters', + flags: { + string: ['kibana'], + default: { + count: 10, + kibana: 'http://elastic:changeme@localhost:5601', + }, + help: ` + --count Number of event filters to create. Default: 10 + --kibana The URL to kibana including credentials. Default: http://elastic:changeme@localhost:5601 + `, + }, + } + ); +}; + +class EventFilterDataLoaderError extends Error { + constructor(message: string, public readonly meta: unknown) { + super(message); + } +} + +const handleThrowAxiosHttpError = (err: AxiosError): never => { + let message = err.message; + + if (err.response) { + message = `[${err.response.status}] ${err.response.data.message ?? err.message} [ ${String( + err.response.config.method + ).toUpperCase()} ${err.response.config.url} ]`; + } + throw new EventFilterDataLoaderError(message, err.toJSON()); +}; + +const createEventFilters: RunFn = async ({ flags, log }) => { + const eventGenerator = new EventFilterGenerator(); + const kbn = new KbnClient({ log, url: flags.kibana as string }); + + await ensureCreateEndpointEventFiltersList(kbn); + + await bluebird.map( + Array.from({ length: (flags.count as unknown) as number }), + () => + kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_ITEM_URL, + body: eventGenerator.generate(), + }) + .catch((e) => handleThrowAxiosHttpError(e)), + { concurrency: 10 } + ); +}; + +const ensureCreateEndpointEventFiltersList = async (kbn: KbnClient) => { + const newListDefinition: CreateExceptionListSchema = { + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + meta: undefined, + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + os_types: [], + tags: [], + type: 'endpoint', + namespace_type: 'agnostic', + }; + + await kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_URL, + body: newListDefinition, + }) + .catch((e) => { + // Ignore if list was already created + if (e.response.status !== 409) { + handleThrowAxiosHttpError(e); + } + }); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js new file mode 100755 index 0000000000000..ca0f4ff9365c5 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./event_filters').cli(); From 6b9ba109587f42e74453a6f50d46fb839374bf7a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 09:50:36 -0700 Subject: [PATCH 076/131] Revert "[Telemetry] enforce import export type (#96199)" This reverts commit ac46802830ceef3f6ce830a5fa68841a95e08102. --- packages/kbn-analytics/tsconfig.json | 1 - packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +--- packages/kbn-telemetry-tools/tsconfig.json | 3 +-- src/plugins/kibana_usage_collection/tsconfig.json | 3 +-- .../telemetry/common/telemetry_config/index.ts | 6 ++++-- src/plugins/telemetry/public/index.ts | 2 +- src/plugins/telemetry/server/index.ts | 9 +++------ .../telemetry_collection/get_data_telemetry/index.ts | 9 +++++++-- .../telemetry/server/telemetry_collection/index.ts | 11 +++++++---- .../telemetry/server/telemetry_repository/index.ts | 2 +- src/plugins/telemetry/tsconfig.json | 3 +-- .../telemetry_collection_manager/server/index.ts | 2 +- .../telemetry_collection_manager/tsconfig.json | 3 +-- .../telemetry_management_section/public/index.ts | 2 +- .../telemetry_management_section/tsconfig.json | 3 +-- src/plugins/usage_collection/public/index.ts | 2 +- .../usage_collection/server/collector/index.ts | 10 ++++------ src/plugins/usage_collection/server/index.ts | 7 ++++--- src/plugins/usage_collection/tsconfig.json | 3 +-- .../telemetry_collection_xpack/server/index.ts | 2 +- .../server/telemetry_collection/index.ts | 2 +- .../plugins/telemetry_collection_xpack/tsconfig.json | 3 +-- 22 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index 80a2255d71805..c2e579e7fdbea 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,7 +7,6 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index f55a9aa80d40d..5d946b73d9759 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,9 +7,7 @@ */ export { ErrorReporter } from './error_reporter'; - -export type { TaskContext } from './task_context'; -export { createTaskContext } from './task_context'; +export { TaskContext, createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 419af1d02f83b..39946fe9907e5 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,8 +6,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", - "isolatedModules": true + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" }, "include": [ "src/**/*", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index ee07dfe589e4a..d664d936f6667 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "common/*", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index cc4ff102742d7..84b6486f35b24 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,5 +9,7 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; -export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; +export { + getTelemetryFailureDetails, + TelemetryFailureDetails, +} from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 47ba7828eaec2..6cca9bdf881dd 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index 1c335426ffd03..debdf7515cd58 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,12 +34,9 @@ export { constants }; export { getClusterUuids, getLocalStats, - DATA_TELEMETRY_ID, - buildDataTelemetryPayload, -} from './telemetry_collection'; - -export type { TelemetryLocalStats, + DATA_TELEMETRY_ID, DataTelemetryIndex, DataTelemetryPayload, + buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index c93b7e872924b..def1131dfb1a3 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,5 +7,10 @@ */ export { DATA_TELEMETRY_ID } from './constants'; -export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; -export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; + +export { + getDataTelemetry, + buildDataTelemetryPayload, + DataTelemetryPayload, + DataTelemetryIndex, +} from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 151e89a11a192..55f9c7f0e624c 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ -export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; -export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; -export { getLocalStats } from './get_local_stats'; -export type { TelemetryLocalStats } from './get_local_stats'; +export { + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, +} from './get_data_telemetry'; +export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 594b53259a65f..4e3f046f7611f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export type { +export { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 6629e479906c9..bdced01d9eb6f 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index c0cd124a132c0..77077b73cf8ad 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export type { +export { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1329979860603..1bba81769f0dd 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index db6ea17556ed3..28b04418f512d 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export type { TelemetryManagementSectionPluginSetup } from './plugin'; +export { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 2daee868ac200..48e40814b8570 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/*", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index 9b009b1d9e264..b9e0e0a8985b1 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index d5e0d95659e58..5f48f9fb93813 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -export { CollectorSet } from './collector_set'; -export type { CollectorSetPublic } from './collector_set'; -export { Collector } from './collector'; -export type { +export { CollectorSet, CollectorSetPublic } from './collector_set'; +export { + Collector, AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -17,5 +16,4 @@ export type { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector } from './usage_collector'; -export type { UsageCollectorOptions } from './usage_collector'; +export { UsageCollector, UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dd9e6644a827d..dfc9d19b69646 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,16 +9,17 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { Collector } from './collector'; -export type { +export { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, + Collector, CollectorFetchContext, } from './collector'; -export type { UsageCollectionSetup } from './plugin'; + +export { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 68a0853994e80..96b2c4d37e17c 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/*", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index aab1bdb58fe59..d924882e17fbd 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export type { ESLicense } from './telemetry_collection'; +export { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index c1a11caf44f24..4599b068b9b38 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export type { ESLicense } from './get_license'; +export { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 1221200c7548c..476f5926f757a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "common/**/*", From 92b659dae04e980b3989ae6b9e1f28ac6ca579f8 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 7 Apr 2021 12:01:26 -0500 Subject: [PATCH 077/131] [Workplace Search] Add AccountHeader to Personal dashboard (#96353) * Revert change to wrap setContext in useEffect A recommendation was made to wrap the setContext call in a previous PR, which lets the app know if the context is org or account, in a useEffect call, for potential performance reasons. Unfortunately, this causes the lifecycle to change so that changing routes from org to personal dashboard does not register the change in time. This commit changes it back to a working state. * Add constants and routes for Account nav * Add AccountHeader component * Add header to layout and fix height The main layout stylesheet, https://github.com/elastic/kibana/blob/master/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss gives a static height that includes the main Kibana navigation. The height with the account nav only is added to the existing privateSourcesLayout css class * Refactor test --- .../account_header/account_header.test.tsx | 53 +++++++++ .../layout/account_header/account_header.tsx | 107 ++++++++++++++++++ .../components/layout/account_header/index.ts | 8 ++ .../components/layout/index.ts | 1 + .../workplace_search/constants.ts | 31 +++++ .../applications/workplace_search/index.tsx | 10 +- .../applications/workplace_search/routes.ts | 2 + .../private_sources_layout.test.tsx | 2 + .../private_sources_layout.tsx | 38 ++++--- .../views/content_sources/sources.scss | 4 + 10 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx new file mode 100644 index 0000000000000..e8035f01a9405 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiHeader, EuiPopover } from '@elastic/eui'; + +import { AccountHeader } from './'; + +describe('AccountHeader', () => { + const mockValues = { + account: { + isAdmin: true, + }, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiHeader)).toHaveLength(1); + }); + + describe('accountSubNav', () => { + it('handles popover trigger click', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + const onClick = popover.dive().find('[data-test-subj="AccountButton"]').prop('onClick'); + onClick!({} as any); + + expect(onClick).toBeDefined(); + }); + + it('handles close popover', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + popover.prop('closePopover')!(); + + expect(popover.prop('isOpen')).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx new file mode 100644 index 0000000000000..a878d87af09e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiHeader, + EuiHeaderLogo, + EuiHeaderLinks, + EuiHeaderSection, + EuiHeaderSectionItem, + EuiText, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; + +import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { WORKPLACE_SEARCH_TITLE, ACCOUNT_NAV } from '../../../constants'; +import { + ALPHA_PATH, + PERSONAL_SOURCES_PATH, + LOGOUT_ROUTE, + KIBANA_ACCOUNT_ROUTE, +} from '../../../routes'; + +export const AccountHeader: React.FC = () => { + const [isPopoverOpen, setPopover] = useState(false); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + const closePopover = () => { + setPopover(false); + }; + + const { + account: { isAdmin }, + } = useValues(AppLogic); + + const accountNavItems = [ + + {/* TODO: Once auth is completed, we need to have non-admins redirect to the self-hosted form */} + {ACCOUNT_NAV.SETTINGS} + , + + {ACCOUNT_NAV.LOGOUT} + , + ]; + + const accountButton = ( + + {ACCOUNT_NAV.ACCOUNT} + + ); + + return ( + + + + + {WORKPLACE_SEARCH_TITLE} + + + + {ACCOUNT_NAV.SOURCES} + + + + + + {isAdmin && ( + {ACCOUNT_NAV.ORG_DASHBOARD} + )} + + + + + {ACCOUNT_NAV.SEARCH} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts new file mode 100644 index 0000000000000..e6cd2516fc03a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 2678b5d01b475..b9a49c416f283 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -7,3 +7,4 @@ export { WorkplaceSearchNav } from './nav'; export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; +export { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index a6e9ce282bf3d..d771673506761 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -9,6 +9,13 @@ import { i18n } from '@kbn/i18n'; import { UPDATE_BUTTON_LABEL, SAVE_BUTTON_LABEL, CANCEL_BUTTON_LABEL } from '../shared/constants'; +export const WORKPLACE_SEARCH_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.title', + { + defaultMessage: 'Workplace Search', + } +); + export const NAV = { OVERVIEW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { defaultMessage: 'Overview', @@ -76,6 +83,30 @@ export const NAV = { }), }; +export const ACCOUNT_NAV = { + SOURCES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.sources.link', { + defaultMessage: 'Content sources', + }), + ORG_DASHBOARD: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link', + { + defaultMessage: 'Go to organizational dashboard', + } + ), + SEARCH: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.search.link', { + defaultMessage: 'Search', + }), + ACCOUNT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.account.link', { + defaultMessage: 'My account', + }), + SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.settings.link', { + defaultMessage: 'Account settings', + }), + LOGOUT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link', { + defaultMessage: 'Logout', + }), +}; + export const MAX_TABLE_ROW_ICONS = 3; export const SOURCE_STATUSES = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 7a76de43be41b..a8d6fc54f7924 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - useEffect(() => { - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' - const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - setContext(isOrganization); + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + + setContext(isOrganization); + + useEffect(() => { setChromeIsVisible(isOrganization); }, [pathname]); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 9e514d7c73493..e08050335671e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -12,6 +12,8 @@ import { docLinks } from '../shared/doc_links'; export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; +export const LOGOUT_ROUTE = '/logout'; +export const KIBANA_ACCOUNT_ROUTE = '/security/account'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 9e3b50ea083eb..7558eb1e4e662 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { SourceSubNav } from './components/source_sub_nav'; @@ -43,6 +44,7 @@ describe('PrivateSourcesLayout', () => { expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); expect(wrapper.find(SourceSubNav)).toHaveLength(1); + expect(wrapper.find(AccountHeader)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index 2a6281075dc40..c565ee5f39a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -12,6 +12,7 @@ import { useValues } from 'kea'; import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { SourceSubNav } from './components/source_sub_nav'; @@ -48,22 +49,25 @@ export const PrivateSourcesLayout: React.FC = ({ : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; return ( - - - - - - - {readOnlyMode && ( - - )} - {children} - - + <> + + + + + + + + {readOnlyMode && ( + + )} + {children} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index abab139e32369..549ca3ae9154e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -20,14 +20,18 @@ .privateSourcesLayout { $sideBarWidth: $euiSize * 30; + $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes + $pageHeight: calc(100vh - #{$consoleHeaderHeight}); left: $sideBarWidth; width: calc(100% - #{$sideBarWidth}); + min-height: $pageHeight; &__sideBar { padding: 32px 40px 40px; width: $sideBarWidth; margin-left: -$sideBarWidth; + height: $pageHeight; } } From 22f7f17fdf021b095ae40d9fb309525d4b208fd8 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 7 Apr 2021 13:07:35 -0400 Subject: [PATCH 078/131] [Fleet] Move fleet server indices creation out of Kibana (#96338) --- .../fleet/server/services/agents/crud.ts | 3 + .../services/api_keys/enrollment_api_key.ts | 2 + .../services/artifacts/artifacts.test.ts | 2 + .../server/services/artifacts/artifacts.ts | 1 + .../fleet_server/elastic_index.test.ts | 141 ----------- .../services/fleet_server/elastic_index.ts | 136 ----------- .../elasticsearch/fleet_actions.json | 33 --- .../elasticsearch/fleet_agents.json | 224 ------------------ .../elasticsearch/fleet_artifacts.json | 48 ---- .../fleet_enrollment_api_keys.json | 32 --- .../elasticsearch/fleet_policies.json | 27 --- .../elasticsearch/fleet_policies_leader.json | 21 -- .../elasticsearch/fleet_servers.json | 47 ---- .../server/services/fleet_server/index.ts | 2 - .../fleet_server/saved_object_migrations.ts | 1 + 15 files changed, 9 insertions(+), 711 deletions(-) delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index ecf18430da668..a23efa1e50fc0 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -125,6 +125,7 @@ export async function getAgentsByKuery( size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, + ignore_unavailable: true, body, }); @@ -180,6 +181,7 @@ export async function countInactiveAgents( index: AGENTS_INDEX, size: 0, track_total_hits: true, + ignore_unavailable: true, body, }); // @ts-expect-error value is number | TotalHits @@ -249,6 +251,7 @@ export async function getAgentByAccessAPIKeyId( ): Promise { const res = await esClient.search({ index: AGENTS_INDEX, + ignore_unavailable: true, q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 643caa8d3bb6f..7059cc96159b9 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -38,6 +38,7 @@ export async function listEnrollmentApiKeys( size: perPage, sort: 'created_at:desc', track_total_hits: true, + ignore_unavailable: true, q: kuery, }); @@ -230,6 +231,7 @@ export async function generateEnrollmentAPIKey( export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, + ignore_unavailable: true, q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts index d4f129a1ae241..5681be3e8793b 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts @@ -152,6 +152,7 @@ describe('When using the artifacts services', () => { expect(esClientMock.search).toHaveBeenCalledWith({ index: FLEET_SERVER_ARTIFACTS_INDEX, sort: 'created:asc', + ignore_unavailable: true, q: '', from: 0, size: 20, @@ -184,6 +185,7 @@ describe('When using the artifacts services', () => { index: FLEET_SERVER_ARTIFACTS_INDEX, sort: 'identifier:desc', q: 'packageName:endpoint', + ignore_unavailable: true, from: 450, size: 50, }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 6e2c22cc2f045..26032ab94dbc8 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -105,6 +105,7 @@ export const listArtifacts = async ( sort: `${sortField}:${sortOrder}`, q: kuery, from: (page - 1) * perPage, + ignore_unavailable: true, size: perPage, }); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts deleted file mode 100644 index 275ea421a508f..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import hash from 'object-hash'; - -import { FLEET_SERVER_INDICES } from '../../../common'; - -import { setupFleetServerIndexes } from './elastic_index'; -import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; -import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; -import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; -import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; -import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; -import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; -import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; - -const FLEET_INDEXES_MIGRATION_HASH: Record = { - '.fleet-actions': hash(EsFleetActionsIndex), - '.fleet-agents': hash(ESFleetAgentIndex), - '.fleet-artifacts': hash(EsFleetArtifactsIndex), - '.fleet-enrollment-apy-keys': hash(ESFleetEnrollmentApiKeysIndex), - '.fleet-policies': hash(ESFleetPoliciesIndex), - '.fleet-policies-leader': hash(ESFleetPoliciesLeaderIndex), - '.fleet-servers': hash(ESFleetServersIndex), -}; - -const getIndexList = (returnAliases: boolean = false): string[] => { - const response = [...FLEET_SERVER_INDICES]; - - if (returnAliases) { - return response.sort(); - } - - return response.map((index) => `${index}_1`).sort(); -}; - -describe('setupFleetServerIndexes ', () => { - it('should create all the indices and aliases if nothings exists', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - await setupFleetServerIndexes(esMock); - - const indexesCreated = esMock.indices.create.mock.calls.map((call) => call[0].index).sort(); - expect(indexesCreated).toEqual(getIndexList()); - const aliasesCreated = esMock.indices.updateAliases.mock.calls - .map((call) => (call[0].body as any)?.actions[0].add.alias) - .sort(); - - expect(aliasesCreated).toEqual(getIndexList(true)); - }); - - it('should not create any indices and create aliases if indices exists but not the aliases', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], - }, - }, - }, - }, - }; - }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - const aliasesCreated = esMock.indices.updateAliases.mock.calls - .map((call) => (call[0].body as any)?.actions[0].add.alias) - .sort(); - - expect(aliasesCreated).toEqual(getIndexList(true)); - }); - - it('should put new indices mapping if the mapping has been updated ', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: 'NOT_VALID_HASH', - }, - }, - }, - }, - }; - }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - const indexesMappingUpdated = esMock.indices.putMapping.mock.calls - .map((call) => call[0].index) - .sort(); - - expect(indexesMappingUpdated).toEqual(getIndexList()); - }); - - it('should not create any indices or aliases if indices and aliases already exists', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], - }, - }, - }, - }, - }; - }); - // @ts-expect-error - esMock.indices.existsAlias.mockResolvedValue({ body: true }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - expect(esMock.indices.updateAliases).not.toBeCalled(); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts deleted file mode 100644 index b0dce60085529..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient } from 'kibana/server'; -import hash from 'object-hash'; - -import type { FLEET_SERVER_INDICES } from '../../../common'; -import { FLEET_SERVER_INDICES_VERSION } from '../../../common'; -import { appContextService } from '../app_context'; - -import { FleetSetupError } from '../../errors'; - -import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; -import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; -import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; -import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; -import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; -import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; -import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; - -const FLEET_INDEXES: Array<[typeof FLEET_SERVER_INDICES[number], any]> = [ - ['.fleet-actions', EsFleetActionsIndex], - ['.fleet-agents', ESFleetAgentIndex], - ['.fleet-artifacts', EsFleetArtifactsIndex], - ['.fleet-enrollment-api-keys', ESFleetEnrollmentApiKeysIndex], - ['.fleet-policies', ESFleetPoliciesIndex], - ['.fleet-policies-leader', ESFleetPoliciesLeaderIndex], - ['.fleet-servers', ESFleetServersIndex], -]; - -export async function setupFleetServerIndexes( - esClient = appContextService.getInternalUserESClient() -) { - await Promise.all( - FLEET_INDEXES.map(async ([indexAlias, indexData]) => { - const index = `${indexAlias}_${FLEET_SERVER_INDICES_VERSION}`; - await createOrUpdateIndex(esClient, index, indexData); - await createAliasIfDoNotExists(esClient, indexAlias, index); - }) - ); -} - -export async function createAliasIfDoNotExists( - esClient: ElasticsearchClient, - alias: string, - index: string -) { - try { - const { body: exists } = await esClient.indices.existsAlias({ - name: alias, - }); - - if (exists === true) { - return; - } - await esClient.indices.updateAliases({ - body: { - actions: [ - { - add: { index, alias }, - }, - ], - }, - }); - } catch (e) { - throw new FleetSetupError(`Create of alias [${alias}] for index [${index}] failed`, e); - } -} - -async function createOrUpdateIndex( - esClient: ElasticsearchClient, - indexName: string, - indexData: any -) { - const resExists = await esClient.indices.exists({ - index: indexName, - }); - - // Support non destructive migration only (adding new field) - if (resExists.body === true) { - return updateIndex(esClient, indexName, indexData); - } - - return createIndex(esClient, indexName, indexData); -} - -async function updateIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { - try { - const res = await esClient.indices.getMapping({ - index: indexName, - }); - - const migrationHash = hash(indexData); - if (res.body[indexName].mappings?._meta?.migrationHash !== migrationHash) { - await esClient.indices.putMapping({ - index: indexName, - body: Object.assign({ - ...indexData.mappings, - _meta: { ...(indexData.mappings._meta || {}), migrationHash }, - }), - }); - } - } catch (e) { - throw new FleetSetupError(`update of index [${indexName}] failed`, e); - } -} - -async function createIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { - try { - const migrationHash = hash(indexData); - await esClient.indices.create({ - index: indexName, - body: { - ...indexData, - settings: { - ...(indexData.settings || {}), - auto_expand_replicas: '0-1', - }, - - mappings: Object.assign({ - ...indexData.mappings, - _meta: { ...(indexData.mappings._meta || {}), migrationHash }, - }), - }, - }); - } catch (err) { - // Swallow already exists errors as concurent Kibana can try to create that indice - if (err?.body?.error?.type !== 'resource_already_exists_exception') { - throw new FleetSetupError(`create of index [${indexName}] Failed`, err); - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json deleted file mode 100644 index 94ad02c6d5f18..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "action_id": { - "type": "keyword" - }, - "agents": { - "type": "keyword" - }, - "data": { - "enabled": false, - "type": "object" - }, - "expiration": { - "type": "date" - }, - "input_type": { - "type": "keyword" - }, - "@timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "user_id" : { - "type": "keyword" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json deleted file mode 100644 index 32caa684679d8..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json +++ /dev/null @@ -1,224 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "action_seq_no": { - "type": "integer", - "index": false - }, - "active": { - "type": "boolean" - }, - "agent": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "default_api_key": { - "type": "keyword" - }, - "default_api_key_id": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_checkin_status": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "properties": { - "elastic": { - "properties": { - "agent": { - "properties": { - "build": { - "properties": { - "original": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "id": { - "type": "keyword" - }, - "log_level": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "upgradeable": { - "type": "boolean" - }, - "version": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 16 - } - } - } - } - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "id": { - "type": "keyword" - }, - "ip": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 64 - } - } - }, - "mac": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 17 - } - } - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "os": { - "properties": { - "family": { - "type": "keyword" - }, - "full": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 128 - } - } - }, - "kernel": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 128 - } - } - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 32 - } - } - } - } - } - } - }, - "packages": { - "type": "keyword" - }, - "policy_coordinator_idx": { - "type": "integer" - }, - "policy_id": { - "type": "keyword" - }, - "policy_output_permissions_hash": { - "type": "keyword" - }, - "policy_revision_idx": { - "type": "integer" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "unenrolled_at": { - "type": "date" - }, - "unenrollment_started_at": { - "type": "date" - }, - "updated_at": { - "type": "date" - }, - "upgrade_started_at": { - "type": "date" - }, - "upgraded_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "object", - "enabled": false - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json deleted file mode 100644 index 1f9643fd599d5..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "identifier": { - "type": "keyword" - }, - "compression_algorithm": { - "type": "keyword", - "index": false - }, - "encryption_algorithm": { - "type": "keyword", - "index": false - }, - "encoded_sha256": { - "type": "keyword" - }, - "encoded_size": { - "type": "long", - "index": false - }, - "decoded_sha256": { - "type": "keyword" - }, - "decoded_size": { - "type": "long", - "index": false - }, - "created": { - "type": "date" - }, - "package_name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "relative_url": { - "type": "keyword" - }, - "body": { - "type": "binary" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json deleted file mode 100644 index fc3898aff55c6..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "keyword" - }, - "api_key_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json deleted file mode 100644 index 50078aaa5ea98..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "coordinator_idx": { - "type": "integer" - }, - "data": { - "enabled": false, - "type": "object" - }, - "default_fleet_server": { - "type": "boolean" - }, - "policy_id": { - "type": "keyword" - }, - "revision_idx": { - "type": "integer" - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json deleted file mode 100644 index ad3dfe64df57c..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "server": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json deleted file mode 100644 index 9ee68735d5b6f..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "agent": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } - }, - "server": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index c2b24ce96c213..94f14fac01d3f 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -10,7 +10,6 @@ import { first } from 'rxjs/operators'; import { appContextService } from '../app_context'; import { licenseService } from '../license'; -import { setupFleetServerIndexes } from './elastic_index'; import { runFleetServerMigration } from './saved_object_migrations'; let _isFleetServerSetup = false; @@ -45,7 +44,6 @@ export async function startFleetServerSetup() { try { // We need licence to be initialized before using the SO service. await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); - await setupFleetServerIndexes(); await runFleetServerMigration(); _isFleetServerSetup = true; } catch (err) { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 78172e4dae366..df8aa7cb01286 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -177,6 +177,7 @@ async function migrateAgentPolicies() { index: AGENT_POLICY_INDEX, q: `policy_id:${agentPolicy.id}`, track_total_hits: true, + ignore_unavailable: true, }); // @ts-expect-error value is number | TotalHits From b89776db6d770370b29f371fadc22a30102d6416 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 19:25:19 +0200 Subject: [PATCH 079/131] Bump color-string from 1.5.3 to 1.5.5 (#96433) --- yarn.lock | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0390c2f7cdaf8..6b977be1797ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9858,15 +9858,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.4.0, color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-string@^1.5.4: +color-string@^1.4.0, color-string@^1.5.2, color-string@^1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== From f1230849616311f2fb34df3ce4e3b2a615bc4625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 7 Apr 2021 19:30:15 +0200 Subject: [PATCH 080/131] [APM] Remove dynamic index pattern caching (#96346) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/index_pattern/get_dynamic_index_pattern.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 8b81101fd2f39..5d5e6eebb4c9f 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -5,7 +5,6 @@ * 2.0. */ -import LRU from 'lru-cache'; import { IndexPatternsFetcher, FieldDescriptor, @@ -19,11 +18,6 @@ export interface IndexPatternTitleAndFields { fields: FieldDescriptor[]; } -const cache = new LRU({ - max: 100, - maxAge: 1000 * 60, -}); - // TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = ({ context, @@ -33,11 +27,6 @@ export const getDynamicIndexPattern = ({ return withApmSpan('get_dynamic_index_pattern', async () => { const indexPatternTitle = context.config['apm_oss.indexPattern']; - const CACHE_KEY = `apm_dynamic_index_pattern_${indexPatternTitle}`; - if (cache.has(CACHE_KEY)) { - return cache.get(CACHE_KEY); - } - const indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser ); @@ -57,11 +46,8 @@ export const getDynamicIndexPattern = ({ title: indexPatternTitle, }; - cache.set(CACHE_KEY, indexPattern); return indexPattern; } catch (e) { - // since `getDynamicIndexPattern` can be called multiple times per request it can be expensive not to cache failed lookups - cache.set(CACHE_KEY, undefined); const notExists = e.output?.statusCode === 404; if (notExists) { context.logger.error( From 8e1bd9ccf35cda70d578633b22eaf21f3c0bf9a4 Mon Sep 17 00:00:00 2001 From: Davey Holler Date: Wed, 7 Apr 2021 10:37:14 -0700 Subject: [PATCH 081/131] App Search Polish (#96345) * Button adjustments to Engine Overview page * Subdued preview panel color * Vertically aligns "manage fields" and "preview" Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app_search/components/engines/components/header.tsx | 2 +- .../app_search/components/engines/engines_overview.tsx | 6 ++++-- .../relevance_tuning_form/relevance_tuning_form.tsx | 1 + .../relevance_tuning/relevance_tuning_preview.tsx | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index fb3b771850a31..df87f2e5230db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -26,7 +26,7 @@ export const EnginesOverviewHeader: React.FC = () => { rightSideItems={[ // eslint-disable-next-line @elastic/eui/href-or-on-click { {canManageEngines && ( @@ -108,6 +109,7 @@ export const EnginesOverview: React.FC = () => { + { return (
+

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 5e5ee2ea8d0f0..911e97de5b53f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -48,7 +48,7 @@ export const RelevanceTuningPreview: React.FC = () => { const { engineName, isMetaEngine } = useValues(EngineLogic); return ( - +

{i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.title', { From c8e23ad440d722b31247fc7ad383b194d74972a5 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 7 Apr 2021 13:07:39 -0500 Subject: [PATCH 082/131] [Fleet] Fixes to preconfigure API (#96094) --- .../common/types/models/preconfiguration.ts | 4 ++ .../server/services/epm/packages/install.ts | 5 ++- .../server/services/preconfiguration.test.ts | 30 +++++++++----- .../fleet/server/services/preconfiguration.ts | 39 +++++++++++++------ .../server/types/models/preconfiguration.ts | 3 ++ 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts index b16234d5a5f97..c9fff1c1581bd 100644 --- a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts @@ -27,3 +27,7 @@ export interface PreconfiguredAgentPolicy extends Omit; } + +export interface PreconfiguredPackage extends Omit { + force?: boolean; +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 7095bb1688c73..168ec55b14876 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -115,8 +115,9 @@ export async function ensureInstalledPackage(options: { pkgName: string; esClient: ElasticsearchClient; pkgVersion?: string; + force?: boolean; }): Promise { - const { savedObjectsClient, pkgName, esClient, pkgVersion } = options; + const { savedObjectsClient, pkgName, esClient, pkgVersion, force } = options; const installedPackage = await isPackageVersionInstalled({ savedObjectsClient, pkgName, @@ -136,7 +137,7 @@ export async function ensureInstalledPackage(options: { savedObjectsClient, pkgkey, esClient, - force: true, + force, }); } else { await installLatestPackage({ diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index bcde8ade427e5..8a885f9c5c821 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -66,9 +66,19 @@ function getPutPreconfiguredPackagesMock() { } jest.mock('./epm/packages/install', () => ({ - ensureInstalledPackage({ pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }) { + ensureInstalledPackage({ + pkgName, + pkgVersion, + force, + }: { + pkgName: string; + pkgVersion: string; + force?: boolean; + }) { const installedPackage = mockInstalledPackages.get(pkgName); - if (installedPackage) return installedPackage; + if (installedPackage) { + if (installedPackage.version === pkgVersion) return installedPackage; + } const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); @@ -138,12 +148,12 @@ describe('policy preconfiguration', () => { soClient, esClient, [], - [{ name: 'test-package', version: '3.0.0' }], + [{ name: 'test_package', version: '3.0.0' }], mockDefaultOutput ); expect(policies.length).toBe(0); - expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0'])); + expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); }); it('should install packages and configure agent policies successfully', async () => { @@ -160,19 +170,19 @@ describe('policy preconfiguration', () => { id: 'test-id', package_policies: [ { - package: { name: 'test-package' }, + package: { name: 'test_package' }, name: 'Test package', }, ], }, ] as PreconfiguredAgentPolicy[], - [{ name: 'test-package', version: '3.0.0' }], + [{ name: 'test_package', version: '3.0.0' }], mockDefaultOutput ); expect(policies.length).toEqual(1); expect(policies[0].id).toBe('mocked-test-id'); - expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0'])); + expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); }); it('should throw an error when trying to install duplicate packages', async () => { @@ -185,13 +195,13 @@ describe('policy preconfiguration', () => { esClient, [], [ - { name: 'test-package', version: '3.0.0' }, - { name: 'test-package', version: '2.0.0' }, + { name: 'test_package', version: '3.0.0' }, + { name: 'test_package', version: '2.0.0' }, ], mockDefaultOutput ) ).rejects.toThrow( - 'Duplicate packages specified in configuration: test-package:3.0.0, test-package:2.0.0' + 'Duplicate packages specified in configuration: test_package-3.0.0, test_package-2.0.0' ); }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index bd1c2ca1f23ef..97480fcf6b2a8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -7,10 +7,9 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { i18n } from '@kbn/i18n'; -import { groupBy } from 'lodash'; +import { groupBy, omit } from 'lodash'; import type { - PackagePolicyPackage, NewPackagePolicy, AgentPolicy, Installation, @@ -18,8 +17,10 @@ import type { NewPackagePolicyInput, NewPackagePolicyInputStream, PreconfiguredAgentPolicy, + PreconfiguredPackage, } from '../../common'; +import { pkgToPkgKey } from './epm/registry'; import { getInstallation } from './epm/packages'; import { ensureInstalledPackage } from './epm/packages/install'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; @@ -32,7 +33,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, policies: PreconfiguredAgentPolicy[] = [], - packages: Array> = [], + packages: PreconfiguredPackage[] = [], defaultOutput: Output ) { // Validate configured packages to ensure there are no version conflicts @@ -45,7 +46,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( // If there are multiple packages with duplicate versions, separate them with semicolons, e.g // package-a:1.0.0, package-a:2.0.0; package-b:1.0.0, package-b:2.0.0 const duplicateList = duplicatePackages - .map(([, versions]) => versions.map((v) => `${v.name}:${v.version}`).join(', ')) + .map(([, versions]) => versions.map((v) => pkgToPkgKey(v)).join(', ')) .join('; '); throw new Error( @@ -60,8 +61,8 @@ export async function ensurePreconfiguredPackagesAndPolicies( // Preinstall packages specified in Kibana config const preconfiguredPackages = await Promise.all( - packages.map(({ name, version }) => - ensureInstalledPreconfiguredPackage(soClient, esClient, name, version) + packages.map(({ name, version, force }) => + ensureInstalledPreconfiguredPackage(soClient, esClient, name, version, force) ) ); @@ -71,7 +72,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy( soClient, esClient, - preconfiguredAgentPolicy + omit(preconfiguredAgentPolicy, 'is_managed') // Don't add `is_managed` until the policy has been fully configured ); if (!created) return { created, policy }; @@ -101,12 +102,22 @@ export async function ensurePreconfiguredPackagesAndPolicies( }) ); - return { created, policy, installedPackagePolicies }; + return { + created, + policy, + installedPackagePolicies, + shouldAddIsManagedFlag: preconfiguredAgentPolicy.is_managed, + }; }) ); for (const preconfiguredPolicy of preconfiguredPolicies) { - const { created, policy, installedPackagePolicies } = preconfiguredPolicy; + const { + created, + policy, + installedPackagePolicies, + shouldAddIsManagedFlag, + } = preconfiguredPolicy; if (created) { await addPreconfiguredPolicyPackages( soClient, @@ -115,6 +126,10 @@ export async function ensurePreconfiguredPackagesAndPolicies( installedPackagePolicies!, defaultOutput ); + // Add the is_managed flag after configuring package policies to avoid errors + if (shouldAddIsManagedFlag) { + agentPolicyService.update(soClient, esClient, policy.id, { is_managed: true }); + } } } @@ -123,7 +138,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( id: p.policy.id, updated_at: p.policy.updated_at, })), - packages: preconfiguredPackages.map((pkg) => `${pkg.name}:${pkg.version}`), + packages: preconfiguredPackages.map((pkg) => pkgToPkgKey(pkg)), }; } @@ -160,13 +175,15 @@ async function ensureInstalledPreconfiguredPackage( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, pkgName: string, - pkgVersion: string + pkgVersion: string, + force?: boolean ) { return ensureInstalledPackage({ savedObjectsClient: soClient, pkgName, esClient, pkgVersion, + force, }); } diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 77a28defaf1bd..0dc0ae8f1db88 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -33,6 +33,7 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( } }, }), + force: schema.maybe(schema.boolean()), }) ); @@ -41,6 +42,8 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( ...AgentPolicyBaseSchema, namespace: schema.maybe(NamespaceSchema), id: schema.oneOf([schema.string(), schema.number()]), + is_default: schema.maybe(schema.boolean()), + is_default_fleet_server: schema.maybe(schema.boolean()), package_policies: schema.arrayOf( schema.object({ name: schema.string(), From 0aa348d9beb8e46344244a66a66d67269908aa1e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 20:22:42 +0200 Subject: [PATCH 083/131] Bump ssri from 8.0.0 to 8.0.1 (#96452) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6b977be1797ec..bb5d9ff8c23aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26242,9 +26242,9 @@ ssri@^7.0.0: minipass "^3.1.1" ssri@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" - integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== dependencies: minipass "^3.1.1" From 88847b98451034f27506502f491d9fc536bec0eb Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 20:31:10 +0200 Subject: [PATCH 084/131] Bump Node.js from version 14.16.0 to 14.16.1 (#96382) --- .ci/Dockerfile | 2 +- .node-version | 2 +- .nvmrc | 2 +- WORKSPACE.bazel | 12 ++++++------ package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 445cc0e51073f..1c59d6d9aaaf8 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.16.0 +ARG NODE_VERSION=14.16.1 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/.nvmrc b/.nvmrc index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 4639414b4564e..e74c646eedeaf 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.2.3") # we can update that rule. node_repositories( node_repositories = { - "14.16.0-darwin_amd64": ("node-v14.16.0-darwin-x64.tar.gz", "node-v14.16.0-darwin-x64", "14ec767e376d1e2e668f997065926c5c0086ec46516d1d45918af8ae05bd4583"), - "14.16.0-linux_arm64": ("node-v14.16.0-linux-arm64.tar.xz", "node-v14.16.0-linux-arm64", "440489a08bfd020e814c9e65017f58d692299ac3f150c8e78d01abb1104c878a"), - "14.16.0-linux_s390x": ("node-v14.16.0-linux-s390x.tar.xz", "node-v14.16.0-linux-s390x", "335348e46f45284b6356416ef58f85602d2dee99094588b65900f6c8839df77e"), - "14.16.0-linux_amd64": ("node-v14.16.0-linux-x64.tar.xz", "node-v14.16.0-linux-x64", "2e079cf638766fedd720d30ec8ffef5d6ceada4e8b441fc2a093cb9a865f4087"), - "14.16.0-windows_amd64": ("node-v14.16.0-win-x64.zip", "node-v14.16.0-win-x64", "716045c2f16ea10ca97bd04cf2e5ef865f9c4d6d677a9bc25e2ea522b594af4f"), + "14.16.1-darwin_amd64": ("node-v14.16.1-darwin-x64.tar.gz", "node-v14.16.1-darwin-x64", "b762b72fc149629b7e394ea9b75a093cad709a9f2f71480942945d8da0fc1218"), + "14.16.1-linux_arm64": ("node-v14.16.1-linux-arm64.tar.xz", "node-v14.16.1-linux-arm64", "b4d474e79f7d33b3b4430fad25c3f836b82ce2d5bb30d4a2c9fa20df027e40da"), + "14.16.1-linux_s390x": ("node-v14.16.1-linux-s390x.tar.xz", "node-v14.16.1-linux-s390x", "af9982fef32e4a3e4a5d66741dcf30ac9c27613bd73582fa1dae1fb25003047a"), + "14.16.1-linux_amd64": ("node-v14.16.1-linux-x64.tar.xz", "node-v14.16.1-linux-x64", "85a89d2f68855282c87851c882d4c4bbea4cd7f888f603722f0240a6e53d89df"), + "14.16.1-windows_amd64": ("node-v14.16.1-win-x64.zip", "node-v14.16.1-win-x64", "e469db37b4df74627842d809566c651042d86f0e6006688f0f5fe3532c6dfa41"), }, - node_version = "14.16.0", + node_version = "14.16.1", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/package.json b/package.json index d5d136305bc9c..a1acf73ea26f0 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "**/typescript": "4.1.3" }, "engines": { - "node": "14.16.0", + "node": "14.16.1", "yarn": "^1.21.1" }, "dependencies": { From 324c6c05a44985fcd9ff15e5285b29b1dcc3c596 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 7 Apr 2021 15:00:55 -0400 Subject: [PATCH 085/131] [Maps] Support query-time runtime fields (#95701) --- .../es_search_source/es_search_source.tsx | 44 +-------------- .../get_docvalue_source_fields.test.ts | 42 +++++++++++++++ .../get_docvalue_source_fields.ts | 54 +++++++++++++++++++ .../apps/maps/embeddable/dashboard.js | 8 +-- .../maps/embeddable/tooltip_filter_actions.js | 4 +- x-pack/test/functional/apps/maps/joins.js | 10 ++-- .../es_archives/maps/kibana/data.json | 3 +- .../es_archives/maps/kibana/mappings.json | 3 ++ 8 files changed, 116 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 168448b6f72a0..ac3a15d2ac490 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -61,54 +61,12 @@ import { DataRequest } from '../../util/data_request'; import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common'; import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; +import { getDocValueAndSourceFields, ScriptField } from './get_docvalue_source_fields'; export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); -export interface ScriptField { - source: string; - lang: string; -} - -function getDocValueAndSourceFields( - indexPattern: IndexPattern, - fieldNames: string[], - dateFormat: string -): { - docValueFields: Array; - sourceOnlyFields: string[]; - scriptFields: Record; -} { - const docValueFields: Array = []; - const sourceOnlyFields: string[] = []; - const scriptFields: Record = {}; - fieldNames.forEach((fieldName) => { - const field = getField(indexPattern, fieldName); - if (field.scripted) { - scriptFields[field.name] = { - script: { - source: field.script || '', - lang: field.lang || '', - }, - }; - } else if (field.readFromDocValues) { - const docValueField = - field.type === 'date' - ? { - field: fieldName, - format: dateFormat, - } - : fieldName; - docValueFields.push(docValueField); - } else { - sourceOnlyFields.push(fieldName); - } - }); - - return { docValueFields, sourceOnlyFields, scriptFields }; -} - export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { readonly _descriptor: ESSearchSourceDescriptor; protected readonly _tooltipFields: ESDocField[]; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts new file mode 100644 index 0000000000000..41744c4343f97 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getDocValueAndSourceFields } from './get_docvalue_source_fields'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { IFieldType } from '../../../../../../../src/plugins/data/common/index_patterns/fields'; + +function createMockIndexPattern(fields: IFieldType[]): IndexPattern { + const indexPattern = { + get fields() { + return { + getByName(fieldname: string) { + return fields.find((f) => f.name === fieldname); + }, + }; + }, + }; + + return (indexPattern as unknown) as IndexPattern; +} + +describe('getDocValueAndSourceFields', () => { + it('should add runtime fields to docvalue fields', () => { + const { docValueFields } = getDocValueAndSourceFields( + createMockIndexPattern([ + { + name: 'foobar', + // @ts-expect-error runtimeField not added yet to IFieldType. API tbd + runtimeField: {}, + }, + ]), + ['foobar'], + 'epoch_millis' + ); + + expect(docValueFields).toEqual(['foobar']); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts new file mode 100644 index 0000000000000..a8d10233b4d54 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { getField } from '../../../../common/elasticsearch_util'; + +export interface ScriptField { + source: string; + lang: string; +} + +export function getDocValueAndSourceFields( + indexPattern: IndexPattern, + fieldNames: string[], + dateFormat: string +): { + docValueFields: Array; + sourceOnlyFields: string[]; + scriptFields: Record; +} { + const docValueFields: Array = []; + const sourceOnlyFields: string[] = []; + const scriptFields: Record = {}; + fieldNames.forEach((fieldName) => { + const field = getField(indexPattern, fieldName); + if (field.scripted) { + scriptFields[field.name] = { + script: { + source: field.script || '', + lang: field.lang || '', + }, + }; + } + // @ts-expect-error runtimeField has not been added to public API yet. exact shape of type TBD. + else if (field.readFromDocValues || field.runtimeField) { + const docValueField = + field.type === 'date' + ? { + field: fieldName, + format: dateFormat, + } + : fieldName; + docValueFields.push(docValueField); + } else { + sourceOnlyFields.push(fieldName); + } + }); + + return { docValueFields, sourceOnlyFields, scriptFields }; +} diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 89c1cbded9a26..e1181119bee09 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -69,7 +69,9 @@ export default function ({ getPageObjects, getService }) { await dashboardPanelActions.openInspectorByTitle('join example'); await retry.try(async () => { const joinExampleRequestNames = await inspector.getRequestNames(); - expect(joinExampleRequestNames).to.equal('geo_shapes*,meta_for_geo_shapes*.shape_name'); + expect(joinExampleRequestNames).to.equal( + 'geo_shapes*,meta_for_geo_shapes*.runtime_shape_name' + ); }); await inspector.close(); @@ -90,7 +92,7 @@ export default function ({ getPageObjects, getService }) { await filterBar.selectIndexPattern('logstash-*'); await filterBar.addFilter('machine.os', 'is', 'win 8'); await filterBar.selectIndexPattern('meta_for_geo_shapes*'); - await filterBar.addFilter('shape_name', 'is', 'alpha'); + await filterBar.addFilter('shape_name', 'is', 'alpha'); // runtime fields do not have autocomplete const gridResponse = await PageObjects.maps.getResponseFromDashboardPanel( 'geo grid vector grid example' @@ -99,7 +101,7 @@ export default function ({ getPageObjects, getService }) { const joinResponse = await PageObjects.maps.getResponseFromDashboardPanel( 'join example', - 'meta_for_geo_shapes*.shape_name' + 'meta_for_geo_shapes*.runtime_shape_name' ); expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 19d77a10a1979..d583e41e5e280 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }) { // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); // expect(hasSourceFilter).to.be(true); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + const hasJoinFilter = await filterBar.hasFilter('runtime_shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); }); }); @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.equal(2); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + const hasJoinFilter = await filterBar.hasFilter('runtime_shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); }); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 8b40651ea5674..181b6928e0ec0 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -39,7 +39,7 @@ export default function ({ getPageObjects, getService }) { it('should re-fetch join with refresh timer', async () => { async function getRequestTimestamp() { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); + await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.runtime_shape_name'); const requestStats = await inspector.getTableData(); const requestTimestamp = PageObjects.maps.getInspectorStatRowHit( requestStats, @@ -121,7 +121,9 @@ export default function ({ getPageObjects, getService }) { }); it('should not apply query to source and apply query to join', async () => { - const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + const joinResponse = await PageObjects.maps.getResponse( + 'meta_for_geo_shapes*.runtime_shape_name' + ); expect(joinResponse.aggregations.join.buckets.length).to.equal(2); }); }); @@ -136,7 +138,9 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to join request', async () => { - const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + const joinResponse = await PageObjects.maps.getResponse( + 'meta_for_geo_shapes*.runtime_shape_name' + ); expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 79f869040f74a..631efb58f9c7b 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -51,6 +51,7 @@ "index": ".kibana", "source": { "index-pattern": { + "runtimeFieldMap" : "{\"runtime_shape_name\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['shape_name'].value)\"}}}", "fields" : "[]", "title": "meta_for_geo_shapes*" }, @@ -498,7 +499,7 @@ "type": "envelope" }, "description": "", - "layerListJSON" : "[{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", + "layerListJSON" : "[{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.runtime_shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.runtime_shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"runtime_shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", "mapStateJSON": "{\"zoom\":3.02,\"center\":{\"lon\":77.33426,\"lat\":-0.04647},\"timeFilters\":{\"from\":\"now-17m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "join example", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"n1t6f\"]}" diff --git a/x-pack/test/functional/es_archives/maps/kibana/mappings.json b/x-pack/test/functional/es_archives/maps/kibana/mappings.json index 7f421123bddf8..f370d4d5fe233 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/maps/kibana/mappings.json @@ -136,6 +136,9 @@ "fieldFormatMap": { "type": "text" }, + "runtimeFieldMap": { + "type": "text" + }, "fields": { "type": "text" }, From 21f38afd27a7cf01ee9059cf9bb73ac0a51af85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Wed, 7 Apr 2021 21:01:54 +0200 Subject: [PATCH 086/131] [SECURITY SOLUTION] Add new exception list type and feature flag for event filtering (#96037) * New exception list type for event filtering * New feature flag for event filtering --- x-pack/plugins/lists/common/schemas/common/schemas.ts | 7 ++++++- .../common/detection_engine/schemas/types/lists.test.ts | 6 +++--- .../security_solution/common/experimental_features.ts | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index e553b65a2f610..f261e4e3eefa6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -212,13 +212,18 @@ export type Tags = t.TypeOf; export const tagsOrUndefined = t.union([tags, t.undefined]); export type TagsOrUndefined = t.TypeOf; -export const exceptionListType = t.keyof({ detection: null, endpoint: null }); +export const exceptionListType = t.keyof({ + detection: null, + endpoint: null, + endpoint_events: null, +}); export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]); export type ExceptionListType = t.TypeOf; export type ExceptionListTypeOrUndefined = t.TypeOf; export enum ExceptionListTypeEnum { DETECTION = 'detection', ENDPOINT = 'endpoint', + ENDPOINT_EVENTS = 'endpoint_events', } export const exceptionListItemType = t.keyof({ simple: null }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts index e331eea51eec0..28b70f51742a7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts @@ -94,7 +94,7 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"', + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', ]); expect(message.schema).toEqual({}); }); @@ -125,8 +125,8 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 19de81cb95c3f..39551e3ee6f1c 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const allowedExperimentalValues = Object.freeze({ fleetServerEnabled: false, trustedAppsByPolicyEnabled: false, + eventFilteringEnabled: false, }); type ExperimentalConfigKeys = Array; From ad06d16beb8a747f03900455510f6588cf51e82e Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 7 Apr 2021 15:20:47 -0400 Subject: [PATCH 087/131] [actions] adds proxyBypassHosts and proxyOnlyHosts Kibana config keys (#95365) resolves https://github.com/elastic/kibana/issues/92949 This PR adds two new Kibana config keys to further customize when the proxy is used when making HTTP requests. Prior to this PR, if a proxy was set via the `xpack.actions.proxyUrl` config key, all requests would be proxied. Now, there's a further refinement in that hostnames can be added to the `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` config keys. Only one of these config keys can be used at a time. If the target URL hostname of the HTTP request is listed in the `proxyBypassHosts` list, the proxy won't be used. If the target URL hostname of the HTTP request is **NOT** listed in the `proxyOnlyHosts` list, the proxy won't be used. Depending on the customer's environment, it may be easier to list the hosts to bypass, or easier to list the hosts that should only be proxied, so they can choose either method. --- docs/settings/alert-action-settings.asciidoc | 6 + .../resources/base/bin/kibana-docker | 2 + .../actions/server/actions_client.test.ts | 2 + .../actions/server/actions_config.test.ts | 79 +++++++++++ .../plugins/actions/server/actions_config.ts | 17 +-- .../lib/axios_utils.test.ts | 98 ++++++++++++- .../builtin_action_types/lib/axios_utils.ts | 2 +- .../lib/get_custom_agents.test.ts | 70 ++++++++- .../lib/get_custom_agents.ts | 25 +++- .../lib/send_email.test.ts | 134 ++++++++++++++++++ .../builtin_action_types/lib/send_email.ts | 13 +- .../server/builtin_action_types/slack.test.ts | 102 +++++++++++++ .../server/builtin_action_types/slack.ts | 8 +- x-pack/plugins/actions/server/config.test.ts | 60 +++++++- x-pack/plugins/actions/server/config.ts | 31 +++- x-pack/plugins/actions/server/plugin.ts | 4 +- x-pack/plugins/actions/server/types.ts | 2 + .../alerting_api_integration/common/config.ts | 14 +- 18 files changed, 645 insertions(+), 24 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 3645499d5f9ff..08cbee8851b98 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -59,6 +59,12 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. +| `xpack.actions.proxyBypassHosts` {ess-icon} + | Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + +| `xpack.actions.proxyOnlyHosts` {ess-icon} + | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + | `xpack.actions.proxyHeaders` {ess-icon} | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 9617a556e2cdd..e0fd649a43df7 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -163,6 +163,8 @@ kibana_vars=( xpack.actions.proxyHeaders xpack.actions.proxyRejectUnauthorizedCertificates xpack.actions.proxyUrl + xpack.actions.proxyBypassHosts + xpack.actions.proxyOnlyHosts xpack.actions.rejectUnauthorized xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a333d86b27129..92d3b4f29d967 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -406,6 +406,8 @@ describe('create()', () => { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index cae6777a82441..36899f7661ba4 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -253,3 +253,82 @@ describe('ensureActionTypeEnabled', () => { expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); }); }); + +describe('getProxySettings', () => { + test('returns undefined when no proxy URL set', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyHeaders: { someHeaderName: 'some header value' }, + proxyBypassHosts: ['avoid-proxy.co'], + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings).toBeUndefined(); + }); + + test('returns proxy url', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + }; + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyUrl).toBe(config.proxyUrl); + }); + + test('returns proxyRejectUnauthorizedCertificates', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: true, + }; + let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: false, + }; + proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false); + }); + + test('returns proxy headers', () => { + const proxyHeaders = { + someHeaderName: 'some header value', + someOtherHeader: 'some other header', + }; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyHeaders, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyHeaders).toEqual(config.proxyHeaders); + }); + + test('returns proxy bypass hosts', () => { + const proxyBypassHosts = ['proxy-bypass-1.elastic.co', 'proxy-bypass-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyBypassHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyBypassHosts).toEqual(new Set(proxyBypassHosts)); + }); + + test('returns proxy only hosts', () => { + const proxyOnlyHosts = ['proxy-only-1.elastic.co', 'proxy-only-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyOnlyHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyOnlyHosts).toEqual(new Set(proxyOnlyHosts)); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index 2787f8f971101..b35a4a0d7b6c5 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -11,17 +11,11 @@ import url from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; -import { ActionsConfig } from './config'; +import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config'; import { ActionTypeDisabledError } from './lib'; import { ProxySettings } from './types'; -export enum AllowedHosts { - Any = '*', -} - -export enum EnabledActionTypes { - Any = '*', -} +export { AllowedHosts, EnabledActionTypes } from './config'; enum AllowListingField { URL = 'url', @@ -93,11 +87,18 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet return { proxyUrl: config.proxyUrl, + proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), + proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, }; } +function arrayAsSet(arr: T[] | undefined): Set | undefined { + if (!arr) return; + return new Set(arr); +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 6a67f4f6752c2..a932b38ede2bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -7,12 +7,16 @@ import axios from 'axios'; import { Agent as HttpsAgent } from 'https'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { getCustomAgents } from './get_custom_agents'; +const TestUrl = 'https://elastic.co/foo/bar/baz'; + const logger = loggingSystemMock.create().get() as jest.Mocked; const configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); @@ -66,17 +70,19 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyRejectUnauthorizedCertificates: true, proxyUrl: 'https://localhost:1212', + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl); const res = await request({ axios, - url: 'http://testProxy', + url: TestUrl, logger, configurationUtilities, }); - expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { + expect(axiosMock).toHaveBeenCalledWith(TestUrl, { method: 'get', data: {}, httpAgent, @@ -94,6 +100,8 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const res = await request({ axios, @@ -116,6 +124,90 @@ describe('request', () => { }); }); + test('it bypasses with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['not-elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + test('it fetch correctly', async () => { const res = await request({ axios, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index f86f3b86c506a..edce369096142 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -30,7 +30,7 @@ export const request = async ({ validateStatus?: (status: number) => boolean; auth?: AxiosBasicCredentials; }): Promise => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url); return await axios(url, { ...rest, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 340ac0f6dda3a..f6d1be9bffc6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -14,6 +14,10 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; +const targetHost = 'elastic.co'; +const targetUrl = `https://${targetHost}/foo/bar/baz`; +const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`; + describe('getCustomAgents', () => { const configurationUtilities = actionsConfigMock.create(); @@ -21,8 +25,10 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); }); @@ -31,15 +37,73 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); test('return default agents for undefined proxy options', () => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); + + test('returns non-proxy agents for matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); + + test('returns proxy agents for non-matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns proxy agents for matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index 92ababf830aa7..ff2d005f4d841 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -19,7 +19,8 @@ interface GetCustomAgentsResponse { export function getCustomAgents( configurationUtilities: ActionsConfigurationUtilities, - logger: Logger + logger: Logger, + url: string ): GetCustomAgentsResponse { const proxySettings = configurationUtilities.getProxySettings(); const defaultAgents = { @@ -33,6 +34,28 @@ export function getCustomAgents( return defaultAgents; } + let targetUrl: URL; + try { + targetUrl = new URL(url); + } catch (err) { + logger.warn(`error determining proxy state for invalid url "${url}", using default agents`); + return defaultAgents; + } + + // filter out hostnames in the proxy bypass or only lists + const { hostname } = targetUrl; + + if (proxySettings.proxyBypassHosts) { + if (proxySettings.proxyBypassHosts.has(hostname)) { + return defaultAgents; + } + } + + if (proxySettings.proxyOnlyHosts) { + if (!proxySettings.proxyOnlyHosts.has(hostname)) { + return defaultAgents; + } + } logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`); let proxyUrl: URL; try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index cc3f03f50c36f..4b45c6d787cd6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,6 +76,8 @@ describe('send_email module', () => { { proxyUrl: 'https://example.com', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, } ); @@ -222,6 +224,138 @@ describe('send_email module', () => { await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops'); }); + + test('it bypasses with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); }); function getSendEmailOptions( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index d4905015f7663..c0a254967b4fe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -63,6 +63,17 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom }; } + let useProxy = !!proxySettings; + + if (host) { + if (proxySettings?.proxyBypassHosts && proxySettings?.proxyBypassHosts?.has(host)) { + useProxy = false; + } + if (proxySettings?.proxyOnlyHosts && !proxySettings?.proxyOnlyHosts?.has(host)) { + useProxy = false; + } + } + if (service === JSON_TRANSPORT_SERVICE) { transportConfig.jsonTransport = true; delete transportConfig.auth; @@ -73,7 +84,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.port = port; transportConfig.secure = !!secure; - if (proxySettings) { + if (proxySettings && useProxy) { transportConfig.tls = { // do not fail on invalid certs if value is false rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6479e29b5a76f..76612696e8e58 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -195,6 +195,8 @@ describe('execute()', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const actionTypeProxy = getActionType({ logger: mockedLogger, @@ -212,6 +214,106 @@ describe('execute()', () => { ); }); + test('ensure proxy bypass will bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy bypass will not bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will not proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + test('renders parameter templates as expected', async () => { expect(actionType.renderParameterTemplates).toBeTruthy(); const paramsWithTemplates = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index a6173229e3267..d0fb4a8c4b935 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -7,6 +7,8 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; @@ -131,13 +133,15 @@ async function slackExecutor( const { message } = params; const proxySettings = configurationUtilities.getProxySettings(); - const customAgents = getCustomAgents(configurationUtilities, logger); + const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl); const agent = webhookUrl.toLowerCase().startsWith('https') ? customAgents.httpsAgent : customAgents.httpAgent; if (proxySettings) { - logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + if (agent instanceof HttpProxyAgent || agent instanceof HttpsProxyAgent) { + logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + } } try { diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index c90a5b2fb9768..0d270512d1dee 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -5,9 +5,17 @@ * 2.0. */ -import { configSchema } from './config'; +import { configSchema, ActionsConfig, getValidatedConfig } from './config'; +import { Logger } from '../../../..//src/core/server'; +import { loggingSystemMock } from '../../../..//src/core/server/mocks'; + +const mockLogger = loggingSystemMock.create().get() as jest.Mocked; describe('config validation', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + test('action defaults', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` @@ -84,6 +92,56 @@ describe('config validation', () => { `"[preconfigured]: invalid preconfigured action id \\"__proto__\\""` ); }); + + test('validates proxyBypassHosts and proxyOnlyHosts', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + let validated: ActionsConfig; + + validated = configSchema.validate({}); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyBypassHosts: bypassHosts, + }); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyOnlyHosts: onlyHosts, + }); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + }); + + test('validates proxyBypassHosts and proxyOnlyHosts used at the same time', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + const config: Record = { + proxyBypassHosts: bypassHosts, + proxyOnlyHosts: onlyHosts, + }; + + let validated: ActionsConfig; + + // the config schema validation validates with both set + validated = configSchema.validate(config); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + + // getValidatedConfig will warn and set onlyHosts to undefined with both set + validated = getValidatedConfig(mockLogger, validated); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.", + ], + ] + `); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b4f29b752957f..450f03308ab0b 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -6,7 +6,15 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { AllowedHosts, EnabledActionTypes } from './actions_config'; +import { Logger } from '../../../../src/core/server'; + +export enum AllowedHosts { + Any = '*', +} + +export enum EnabledActionTypes { + Any = '*', +} const preconfiguredActionSchema = schema.object({ name: schema.string({ minLength: 1 }), @@ -36,11 +44,32 @@ export const configSchema = schema.object({ proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), + proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), + proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), rejectUnauthorized: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; +// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on +// simultaneous usage in the config validator directly, but there's no good way to express +// this relationship in the cloud config constraints, so we're doing it "live". +export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig { + const proxyBypassHosts = originalConfig.proxyBypassHosts; + const proxyOnlyHosts = originalConfig.proxyOnlyHosts; + + if (proxyBypassHosts && proxyOnlyHosts) { + logger.warn( + 'The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.' + ); + const tmp: Record = originalConfig; + delete tmp.proxyOnlyHosts; + return tmp as ActionsConfig; + } + + return originalConfig; +} + const invalidActionIds = new Set(['', '__proto__', 'constructor']); function validatePreconfigured(preconfigured: Record): string | undefined { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5ec9241533b3c..bfe3b0a09ff2e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -30,7 +30,7 @@ import { SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; -import { ActionsConfig } from './config'; +import { ActionsConfig, getValidatedConfig } from './config'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; @@ -141,8 +141,8 @@ export class ActionsPlugin implements Plugin(); this.logger = initContext.logger.get('actions'); + this.actionsConfig = getValidatedConfig(this.logger, initContext.config.get()); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; this.kibanaIndexConfig = initContext.config.legacy.get(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 4e3916f5d6e23..6830f013ade5f 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -133,6 +133,8 @@ export interface ActionTaskExecutorParams { export interface ProxySettings { proxyUrl: string; + proxyBypassHosts: Set | undefined; + proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; proxyRejectUnauthorizedCertificates: boolean; } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 560ff6c0b317f..beb639eb46334 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -68,12 +68,24 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const proxyPort = process.env.ALERTING_PROXY_PORT ?? (await getPort({ port: getPort.makeRange(6200, 6300) })); + + // If testing with proxy, also test proxyOnlyHosts for this proxy; + // all the actions are assumed to be acccessing localhost anyway. + // If not testing with proxy, set a bogus proxy up, and set the bypass + // flag for all our localhost actions to bypass it. Currently, + // security_and_spaces uses enableActionsProxy: true, and spaces_only + // uses enableActionsProxy: false. + const proxyHosts = ['localhost', 'some.non.existent.com']; const actionsProxyUrl = options.enableActionsProxy ? [ `--xpack.actions.proxyUrl=http://localhost:${proxyPort}`, + `--xpack.actions.proxyOnlyHosts=${JSON.stringify(proxyHosts)}`, '--xpack.actions.proxyRejectUnauthorizedCertificates=false', ] - : []; + : [ + `--xpack.actions.proxyUrl=http://elastic.co`, + `--xpack.actions.proxyBypassHosts=${JSON.stringify(proxyHosts)}`, + ]; return { testFiles: [require.resolve(`../${name}/tests/`)], From 71c326c8bff4c0b7f9286d18e6239da91f2cbfb6 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 7 Apr 2021 15:43:06 -0400 Subject: [PATCH 088/131] handle runtime fields in validation step (#96340) --- .../ml/common/types/data_frame_analytics.ts | 3 +- .../models/data_frame_analytics/validation.ts | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index d9632f4d4a83b..ff5069e7d59ad 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import type { estypes } from '@elastic/elasticsearch'; import { RuntimeMappings } from './fields'; import { EsErrorBody } from '../util/errors'; @@ -75,7 +76,7 @@ export interface DataFrameAnalyticsConfig { }; source: { index: IndexName | IndexName[]; - query?: any; + query?: estypes.QueryContainer; runtime_mappings?: RuntimeMappings; }; analysis: AnalysisConfig; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 3f0a02f5eaad8..bbfc304958f9a 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -195,12 +195,13 @@ function getTrainingPercentMessage(trainingDocs: number) { async function getValidationCheckMessages( asCurrentUser: IScopedClusterClient['asCurrentUser'], analyzedFields: string[], - index: string | string[], analysisConfig: AnalysisConfig, - query: estypes.QueryContainer = defaultQuery + source: DataFrameAnalyticsConfig['source'] ) { const analysisType = getAnalysisType(analysisConfig); const depVar = getDependentVar(analysisConfig); + const index = source.index; + const query = source.query || defaultQuery; const messages = []; const emptyFields: string[] = []; const percentEmptyLimit = FRACTION_EMPTY_LIMIT * 100; @@ -236,6 +237,7 @@ async function getValidationCheckMessages( size: 0, track_total_hits: true, body: { + ...(source.runtime_mappings ? { runtime_mappings: source.runtime_mappings } : {}), query, aggs, }, @@ -247,21 +249,22 @@ async function getValidationCheckMessages( if (body.aggregations) { // @ts-expect-error Object.entries(body.aggregations).forEach(([aggName, { doc_count: docCount, value }]) => { - const empty = docCount / totalDocs; + if (docCount !== undefined) { + const empty = docCount / totalDocs; + if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { + emptyFields.push(aggName); - if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { - emptyFields.push(aggName); - - if (aggName === depVar) { - depVarValid = false; - dependentVarWarningMessage.text = i18n.translate( - 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', - { - defaultMessage: - 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', - values: { percentEmpty: percentEmptyLimit }, - } - ); + if (aggName === depVar) { + depVarValid = false; + dependentVarWarningMessage.text = i18n.translate( + 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', + { + defaultMessage: + 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', + values: { percentEmpty: percentEmptyLimit }, + } + ); + } } } @@ -374,9 +377,8 @@ export async function validateAnalyticsJob( const messages = await getValidationCheckMessages( client.asCurrentUser, job.analyzed_fields.includes, - job.source.index, job.analysis, - job.source.query + job.source ); return messages; } From d8ef85e85ba18dd0bccc65b6e9edf483d80f287b Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 7 Apr 2021 14:08:28 -0700 Subject: [PATCH 089/131] [globby] normalize paths for windows support (#96476) Co-authored-by: spalger --- .../simple_kibana_platform_plugin_discovery.ts | 3 ++- .../run_failed_tests_reporter_cli.ts | 5 ++++- .../package_json/find_used_dependencies.ts | 18 ++++++++++-------- .../integration_tests/ref_output_cache.test.ts | 6 +++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts index 26b1a6fa2e804..2381faefbff29 100644 --- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts @@ -9,6 +9,7 @@ import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; @@ -32,7 +33,7 @@ export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPa ), ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) - ); + ).map((path) => normalize(path)); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => // absolute paths returned from globby are using normalize or diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 8ef11e2dba462..63eca93def64d 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -11,6 +11,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { run, createFailError, createFlagError } from '@kbn/dev-utils'; import globby from 'globby'; +import normalize from 'normalize-path'; import { getFailures, TestFailure } from './get_failures'; import { GithubApi, GithubIssueMini } from './github_api'; @@ -61,7 +62,9 @@ export function runFailedTestsReporterCli() { throw createFlagError('Missing --build-url or process.env.BUILD_URL'); } - const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS; + const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => + normalize(Path.resolve(p)) + ); log.info('Searching for reports at', patterns); const reportPaths = await globby(patterns, { absolute: true, diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index 3a296ec76f3e6..004e17b87ac8b 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; // @ts-ignore import { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser'; @@ -21,16 +23,16 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Define the entry points for the server code in order to // start here later looking for the server side dependencies const mainCodeEntries = [ - `${baseDir}/src/cli/dist.js`, - `${baseDir}/src/cli_keystore/dist.js`, - `${baseDir}/src/cli_plugin/dist.js`, + Path.resolve(baseDir, `src/cli/dist.js`), + Path.resolve(baseDir, `src/cli_keystore/dist.js`), + Path.resolve(baseDir, `src/cli_plugin/dist.js`), ]; const discoveredPluginEntries = await globby([ - `${baseDir}/src/plugins/*/server/index.js`, - `!${baseDir}/src/plugins/**/public`, - `${baseDir}/x-pack/plugins/*/server/index.js`, - `!${baseDir}/x-pack/plugins/**/public`, + normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`, + normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`, ]); // It will include entries that cannot be discovered @@ -40,7 +42,7 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Another way would be to include an index file and import all the functions // using named imports const dynamicRequiredEntries = await globby([ - `${baseDir}/src/plugins/vis_type_timelion/server/**/*.js`, + normalize(Path.resolve(baseDir, 'src/plugins/vis_type_timelion/server/**/*.js')), ]); // Compose all the needed entries diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts index 2bc75785ee6a7..7347529239176 100644 --- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts +++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts @@ -12,6 +12,7 @@ import Fs from 'fs'; import del from 'del'; import cpy from 'cpy'; import globby from 'globby'; +import normalize from 'normalize-path'; import { ToolingLog, createAbsolutePathSerializer, @@ -98,7 +99,10 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and const files = Object.fromEntries( globby - .sync(outDirs, { dot: true }) + .sync( + outDirs.map((p) => normalize(p)), + { dot: true } + ) .map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')]) ); From e6e3b16ee1d7a98f2f5b6fcb2f00a62c6a12a301 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 7 Apr 2021 14:18:36 -0700 Subject: [PATCH 090/131] [Enterprise Search] Change last breadcrumb to inactive/non-linked breadcrumb (#96489) * Update our EUI breadcrumb helper to skip generating links for the last breadcrumb in the list * Fix useEuiBreadcrumbs tests - add a describe block to make it clear we're testing link behavior in non-last breadcrumbs - add a helper that automatically adds a last breadcrumb so that link generation still works * Add comment/note as to why I didn't add last-breadcrumb-specific logic to useGenerateBreadcrumbs --- .../generate_breadcrumbs.test.ts | 75 +++++++++---------- .../kibana_chrome/generate_breadcrumbs.ts | 10 ++- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 0bf7d618c33b3..c05c4dcbdddc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -14,6 +14,7 @@ jest.mock('../react_router_helpers', () => ({ import { letBrowserHandleEvent } from '../react_router_helpers'; import { + Breadcrumb, useGenerateBreadcrumbs, useEuiBreadcrumbs, useEnterpriseSearchBreadcrumbs, @@ -40,6 +41,9 @@ describe('useGenerateBreadcrumbs', () => { { text: 'Groups', path: '/groups' }, { text: 'Example Group Name', path: '/groups/{id}' }, { text: 'Source Prioritization', path: '/groups/{id}/source_prioritization' }, + // Note: We're still generating a path for the last breadcrumb even though useEuiBreadcrumbs + // will not render a link for it. This is because it's easier to keep our last-breadcrumb-specific + // logic in one place, & this way we still have a current path if (for some reason) we need it later. ]); }); @@ -89,48 +93,51 @@ describe('useEuiBreadcrumbs', () => { }, { text: 'World', - href: '/app/enterprise_search/world', - onClick: expect.any(Function), + // Per EUI best practices, the last breadcrumb is inactive/is not a link }, ]); }); - it('prevents default navigation and uses React Router history on click', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/test' }])[0] as any; + describe('link behavior for non-last breadcrumbs', () => { + // Test helper - adds a 2nd dummy breadcrumb so that paths from the first breadcrumb are generated + const useEuiBreadcrumb = (breadcrumb: Breadcrumb) => + useEuiBreadcrumbs([breadcrumb, { text: '' }])[0] as any; - expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); - expect(mockHistory.createHref).toHaveBeenCalled(); + it('prevents default navigation and uses React Router history on click', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test' }); - const event = { preventDefault: jest.fn() }; - breadcrumb.onClick(event); + expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); + expect(mockHistory.createHref).toHaveBeenCalled(); - expect(event.preventDefault).toHaveBeenCalled(); - expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); - }); + const event = { preventDefault: jest.fn() }; + breadcrumb.onClick(event); - it('does not call createHref if shouldNotCreateHref is passed', () => { - const breadcrumb = useEuiBreadcrumbs([ - { text: '', path: '/test', shouldNotCreateHref: true }, - ])[0] as any; + expect(event.preventDefault).toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); + }); - expect(breadcrumb.href).toEqual('/test'); - expect(mockHistory.createHref).not.toHaveBeenCalled(); - }); + it('does not call createHref if shouldNotCreateHref is passed', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test', shouldNotCreateHref: true }); - it('does not prevent default browser behavior on new tab/window clicks', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/' }])[0] as any; + expect(breadcrumb.href).toEqual('/test'); + expect(mockHistory.createHref).not.toHaveBeenCalled(); + }); - (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); - breadcrumb.onClick(); + it('does not prevent default browser behavior on new tab/window clicks', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/' }); - expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); - }); + (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); + breadcrumb.onClick(); + + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); + }); - it('does not generate link behavior if path is excluded', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0]; + it('does not generate link behavior if path is excluded', () => { + const breadcrumb = useEuiBreadcrumb({ text: 'Unclickable breadcrumb' }); - expect(breadcrumb.href).toBeUndefined(); - expect(breadcrumb.onClick).toBeUndefined(); + expect(breadcrumb.href).toBeUndefined(); + expect(breadcrumb.onClick).toBeUndefined(); + }); }); }); @@ -164,8 +171,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -174,8 +179,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', - href: '/app/enterprise_search/overview', - onClick: expect.any(Function), }, ]); }); @@ -219,8 +222,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/app_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -234,8 +235,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'App Search', - href: '/app/enterprise_search/app_search/', - onClick: expect.any(Function), }, ]); }); @@ -279,8 +278,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/workplace_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -294,8 +291,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Workplace Search', - href: '/app/enterprise_search/workplace_search/', - onClick: expect.any(Function), }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 908cc0601ab9c..5855dc6990f6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -24,7 +24,7 @@ import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; * Types */ -interface Breadcrumb { +export interface Breadcrumb { text: string; path?: string; // Used to navigate outside of the React Router basename, @@ -64,16 +64,20 @@ export const useGenerateBreadcrumbs = (trail: BreadcrumbTrail): Breadcrumbs => { /** * Convert IBreadcrumb objects to React-Router-friendly EUI breadcrumb objects * https://elastic.github.io/eui/#/navigation/breadcrumbs + * + * NOTE: Per EUI best practices, we remove the link behavior and + * generate an inactive breadcrumb for the last breadcrumb in the list. */ export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => { const { navigateToUrl, history } = useValues(KibanaLogic); const { http } = useValues(HttpLogic); - return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { + return breadcrumbs.map(({ text, path, shouldNotCreateHref }, i) => { const breadcrumb: EuiBreadcrumb = { text }; + const isLastBreadcrumb = i === breadcrumbs.length - 1; - if (path) { + if (path && !isLastBreadcrumb) { breadcrumb.href = createHref(path, { history, http }, { shouldNotCreateHref }); breadcrumb.onClick = (event) => { if (letBrowserHandleEvent(event)) return; From 8d2d2ad864fb4761725f118e67f050b73d7df454 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 7 Apr 2021 16:51:05 -0500 Subject: [PATCH 091/131] Replace `EuiPanel` with `EuiCard` when using beta badges (#96147) In elastic/eui#4649 the `betaBadgeLabel` and related props have been removed from `EuiPanel` and it's now recommended to use an `EuiCard` instead. Replace these in APM and Observability plugins and update stories so examples can be viewed. --- .../components/app/ServiceMap/index.tsx | 2 +- .../LinkPreview.tsx | 133 ---------------- .../CreateEditCustomLinkFlyout/index.tsx | 2 +- .../link_preview.stories.tsx | 39 +++++ .../link_preview.test.tsx | 2 +- .../link_preview.tsx | 147 ++++++++++++++++++ .../Settings/CustomizeUI/CustomLink/index.tsx | 2 +- .../app/Settings/anomaly_detection/index.tsx | 2 +- .../components/app/correlations/index.tsx | 2 +- .../components/shared/LicensePrompt/index.tsx | 63 -------- .../shared/license_prompt/index.tsx | 59 +++++++ .../license_prompt.stories.tsx} | 20 ++- .../components/app/fleet_panel/index.tsx | 59 +++---- .../public/pages/landing/landing.stories.tsx | 41 +++++ 14 files changed, 327 insertions(+), 246 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx delete mode 100644 x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx rename x-pack/plugins/apm/public/components/shared/{LicensePrompt/LicensePrompt.stories.tsx => license_prompt/license_prompt.stories.tsx} (61%) create mode 100644 x-pack/plugins/observability/public/pages/landing/landing.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7ef3cbca3ad2f..b338d1e4ab03d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -19,7 +19,7 @@ import { useLicenseContext } from '../../../context/license/use_license_context' import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { DatePicker } from '../../shared/DatePicker'; -import { LicensePrompt } from '../../shared/LicensePrompt'; +import { LicensePrompt } from '../../shared/license_prompt'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscape_options'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx deleted file mode 100644 index 0312b802df173..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { - EuiPanel, - EuiText, - EuiSpacer, - EuiLink, - EuiToolTip, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; -import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; -import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; - -interface Props { - label: string; - url: string; - filters: Filter[]; -} - -const fetchTransaction = debounce( - async (filters: Filter[], callback: (transaction: Transaction) => void) => { - const transaction = await callApmApi({ - signal: null, - endpoint: 'GET /api/apm/settings/custom_links/transaction', - params: { query: convertFiltersToQuery(filters) }, - }); - callback(transaction); - }, - 1000 -); - -const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); - -export function LinkPreview({ label, url, filters }: Props) { - const [transaction, setTransaction] = useState(); - - useEffect(() => { - /* - React throwns "Can't perform a React state update on an unmounted component" - It happens when the Custom Link flyout is closed before the return of the api request. - To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. - */ - let isUnmounted = false; - fetchTransaction(filters, (_transaction: Transaction) => { - if (!isUnmounted) { - setTransaction(_transaction); - } - }); - return () => { - isUnmounted = true; - }; - }, [filters]); - - const { formattedUrl, error } = replaceTemplateVariables(url, transaction); - - return ( - - - {label - ? label - : i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.label', - { defaultMessage: 'Elastic.co' } - )} - - - - {url ? ( - - {formattedUrl} - - ) : ( - i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.url', - { defaultMessage: 'https://www.elastic.co' } - ) - )} - - - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', - { - defaultMessage: - 'Test your link with values from an example transaction document based on the filters above.', - } - )} - - - - - {error && ( - - - - )} - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index ccd2b0d425743..dfe768735d19b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -22,7 +22,7 @@ import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; import { LinkSection } from './LinkSection'; import { saveCustomLink } from './saveCustomLink'; -import { LinkPreview } from './LinkPreview'; +import { LinkPreview } from './link_preview'; import { Documentation } from './Documentation'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx new file mode 100644 index 0000000000000..3bf17a733bf8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ComponentProps } from 'react'; +import { CoreStart } from 'kibana/public'; +import { createCallApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { LinkPreview } from './link_preview'; + +export default { + title: + 'app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview', + component: LinkPreview, +}; + +export function Example({ + filters, + label, + url, +}: ComponentProps) { + const coreMock = ({ + http: { + get: async () => ({ transaction: { id: '0' } }), + }, + uiSettings: { get: () => false }, + } as unknown) as CoreStart; + + createCallApmApi(coreMock); + + return ; +} +Example.args = { + filters: [], + label: 'Example label', + url: 'https://example.com', +} as ComponentProps; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 6348157104287..6a6db40892e10 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview'; +import { LinkPreview } from '../CreateEditCustomLinkFlyout/link_preview'; import { render, getNodeText, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx new file mode 100644 index 0000000000000..726d4ba0d65ee --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiPanel, + EuiText, + EuiSpacer, + EuiLink, + EuiToolTip, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; +import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; + +export interface LinkPreviewProps { + label: string; + url: string; + filters: Filter[]; +} + +const fetchTransaction = debounce( + async (filters: Filter[], callback: (transaction: Transaction) => void) => { + const transaction = await callApmApi({ + signal: null, + endpoint: 'GET /api/apm/settings/custom_links/transaction', + params: { query: convertFiltersToQuery(filters) }, + }); + callback(transaction); + }, + 1000 +); + +const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); + +export function LinkPreview({ label, url, filters }: LinkPreviewProps) { + const [transaction, setTransaction] = useState(); + + useEffect(() => { + /* + React throwns "Can't perform a React state update on an unmounted component" + It happens when the Custom Link flyout is closed before the return of the api request. + To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. + */ + let isUnmounted = false; + fetchTransaction(filters, (_transaction: Transaction) => { + if (!isUnmounted) { + setTransaction(_transaction); + } + }); + return () => { + isUnmounted = true; + }; + }, [filters]); + + const { formattedUrl, error } = replaceTemplateVariables(url, transaction); + + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.previewSectionTitle', + { + defaultMessage: 'Preview', + } + )} +

+
+ + + + {label + ? label + : i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.label', + { defaultMessage: 'Elastic.co' } + )} + + + + {url ? ( + + {formattedUrl} + + ) : ( + i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.url', + { defaultMessage: 'https://www.elastic.co' } + ) + )} + + + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', + { + defaultMessage: + 'Test your link with values from an example transaction document based on the filters above.', + } + )} + + + + + {error && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 49fa3eab47862..ab18a31e76917 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -20,7 +20,7 @@ import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; import { useLicenseContext } from '../../../../../context/license/use_license_context'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../../shared/license_prompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 72f0249f07bf6..62b39664cf63d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -14,7 +14,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { LicensePrompt } from '../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../shared/license_prompt'; import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index e0651edbeb79b..62c547aa69e0d 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -34,7 +34,7 @@ import { } from '../../../../../observability/public'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useLicenseContext } from '../../../context/license/use_license_context'; -import { LicensePrompt } from '../../shared/LicensePrompt'; +import { LicensePrompt } from '../../shared/license_prompt'; import { IUrlParams } from '../../../context/url_params_context/types'; const latencyTab = { diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx deleted file mode 100644 index 97a48a61e47cc..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; - -interface Props { - text: string; - showBetaBadge?: boolean; -} - -export function LicensePrompt({ text, showBetaBadge = false }: Props) { - const licensePageUrl = useKibanaUrl( - '/app/management/stack/license_management' - ); - - const renderLicenseBody = ( - - {i18n.translate('xpack.apm.license.title', { - defaultMessage: 'Start free 30-day trial', - })} -

- } - body={

{text}

} - actions={ - - {i18n.translate('xpack.apm.license.button', { - defaultMessage: 'Start trial', - })} - - } - /> - ); - - const renderWithBetaBadge = ( - - {renderLicenseBody} - - ); - - return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}; -} diff --git a/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx new file mode 100644 index 0000000000000..0950cff5127fc --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiCard, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; + +export interface LicensePromptProps { + text: string; + showBetaBadge?: boolean; +} + +export function LicensePrompt({ + text, + showBetaBadge = false, +}: LicensePromptProps) { + const licensePageUrl = useKibanaUrl( + '/app/management/stack/license_management' + ); + + return ( + {text}} + footer={ + + {i18n.translate('xpack.apm.license.button', { + defaultMessage: 'Start trial', + })} + + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx rename to x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx index 57f782a020082..35e22b50306d9 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ComponentType } from 'react'; +import React, { ComponentProps, ComponentType } from 'react'; import { LicensePrompt } from '.'; import { ApmPluginContext, @@ -17,19 +17,25 @@ const contextMock = ({ } as unknown) as ApmPluginContextValue; export default { - title: 'app/LicensePrompt', + title: 'shared/LicensePrompt', component: LicensePrompt, decorators: [ (Story: ComponentType) => ( - {' '} + ), ], }; -export function Example() { - return ( - - ); +export function Example({ + showBetaBadge, + text, +}: ComponentProps) { + return ; } +Example.args = { + showBetaBadge: false, + text: + 'To create Feature name, you must be subscribed to an Elastic X license or above.', +} as ComponentProps; diff --git a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx index b1ca3c614fc70..fce1cde38f587 100644 --- a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx @@ -5,53 +5,38 @@ * 2.0. */ -import React from 'react'; -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; +import { EuiCard, EuiLink, EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; +import React from 'react'; import { usePluginContext } from '../../../hooks/use_plugin_context'; export function FleetPanel() { const { core } = usePluginContext(); return ( - - - - -

- {i18n.translate('xpack.observability.fleet.title', { - defaultMessage: 'Have you seen our new Fleet?', - })} -

-
-
- - - {i18n.translate('xpack.observability.fleet.text', { - defaultMessage: - 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', - })} - - - - - {i18n.translate('xpack.observability.fleet.button', { - defaultMessage: 'Try Fleet Beta', - })} - - -
-
+ description={ + + {i18n.translate('xpack.observability.fleet.text', { + defaultMessage: + 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', + })} + + } + footer={ + + {i18n.translate('xpack.observability.fleet.button', { + defaultMessage: 'Try Fleet Beta', + })} + + } + title={i18n.translate('xpack.observability.fleet.title', { + defaultMessage: 'Have you seen our new Fleet?', + })} + /> ); } diff --git a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx new file mode 100644 index 0000000000000..86922b045c742 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ComponentType } from 'react'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { PluginContext, PluginContextValue } from '../../context/plugin_context'; +import { LandingPage } from './'; + +export default { + title: 'app/Landing', + component: LandingPage, + decorators: [ + (Story: ComponentType) => { + const pluginContextValue = ({ + appMountParameters: { setHeaderActionMenu: () => {} }, + core: { + http: { + basePath: { + prepend: () => '', + }, + }, + }, + } as unknown) as PluginContextValue; + return ( + + + + + + ); + }, + ], +}; + +export function Example() { + return ; +} From c2d5fa1dda95d6689c064f48bfa6bbab1605f494 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 7 Apr 2021 15:06:44 -0700 Subject: [PATCH 092/131] [Actions] Added action configuration settings `maxResponseContentLength` and `responseTimeout`. (#96355) * [Actions] Added action configuration settings `maxResponseContentLength` and `responseTimeout` which define max response content size (in bytes) and awaiting timeout for action executions based on axios requests. * replaced pasceDuration with moment * fixed due to comments * renamed internal options --- docs/settings/alert-action-settings.asciidoc | 7 +++++ .../resources/base/bin/kibana-docker | 2 ++ .../actions/server/actions_client.test.ts | 4 +++ .../actions/server/actions_config.mock.ts | 4 +++ .../actions/server/actions_config.test.ts | 16 ++++++++++ .../plugins/actions/server/actions_config.ts | 11 ++++++- .../server/builtin_action_types/email.test.ts | 2 ++ .../lib/axios_utils.test.ts | 19 ++++++++++++ .../builtin_action_types/lib/axios_utils.ts | 3 ++ .../server/builtin_action_types/teams.test.ts | 2 ++ .../builtin_action_types/webhook.test.ts | 29 +++++++++++++++++++ .../server/builtin_action_types/webhook.ts | 5 +++- x-pack/plugins/actions/server/config.test.ts | 8 +++++ x-pack/plugins/actions/server/config.ts | 2 ++ x-pack/plugins/actions/server/plugin.test.ts | 6 ++++ x-pack/plugins/actions/server/types.ts | 5 ++++ 16 files changed, 123 insertions(+), 2 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 08cbee8851b98..20bbbcf874c05 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -77,6 +77,13 @@ a|`xpack.actions.` + As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates. +| `xpack.actions.maxResponseContentLength` {ess-icon} + | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB). + +| `xpack.actions.responseTimeout` {ess-icon} + | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as [ms|s|m|h|d|w|M|Y], for example, '20m', '24h', '7d', '1w'. Defaults to 60s. + + |=== [float] diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index e0fd649a43df7..6cc94208fbcce 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -166,6 +166,8 @@ kibana_vars=( xpack.actions.proxyBypassHosts xpack.actions.proxyOnlyHosts xpack.actions.rejectUnauthorized + xpack.actions.maxResponseContentLength + xpack.actions.responseTimeout xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 92d3b4f29d967..6544a3c426e42 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -6,6 +6,8 @@ */ import { schema } from '@kbn/config-schema'; +import moment from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; @@ -408,6 +410,8 @@ describe('create()', () => { rejectUnauthorized: true, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration('60s'), }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index 012cd63be2702..76f6a62ce6597 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -17,6 +17,10 @@ const createActionsConfigMock = () => { ensureActionTypeEnabled: jest.fn().mockReturnValue({}), isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), getProxySettings: jest.fn().mockReturnValue(undefined), + getResponseSettings: jest.fn().mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 36899f7661ba4..c81f1f4a4bf2e 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { ActionsConfig } from './config'; import { getActionsConfigurationUtilities, AllowedHosts, EnabledActionTypes, } from './actions_config'; +import moment from 'moment'; const defaultActionsConfig: ActionsConfig = { enabled: false, @@ -19,6 +21,8 @@ const defaultActionsConfig: ActionsConfig = { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }; describe('ensureUriAllowed', () => { @@ -254,6 +258,18 @@ describe('ensureActionTypeEnabled', () => { }); }); +describe('getResponseSettingsFromConfig', () => { + test('returns expected parsed values for default config for responseTimeout and maxResponseContentLength', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + }; + expect(getActionsConfigurationUtilities(config).getResponseSettings()).toEqual({ + timeout: 60000, + maxContentLength: 1000000, + }); + }); +}); + describe('getProxySettings', () => { test('returns undefined when no proxy URL set', () => { const config: ActionsConfig = { diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b35a4a0d7b6c5..4c73cab76f9e8 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -13,7 +13,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings } from './types'; +import { ProxySettings, ResponseSettings } from './types'; export { AllowedHosts, EnabledActionTypes } from './config'; @@ -31,6 +31,7 @@ export interface ActionsConfigurationUtilities { ensureActionTypeEnabled: (actionType: string) => void; isRejectUnauthorizedCertificatesEnabled: () => boolean; getProxySettings: () => undefined | ProxySettings; + getResponseSettings: () => ResponseSettings; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -99,6 +100,13 @@ function arrayAsSet(arr: T[] | undefined): Set | undefined { return new Set(arr); } +function getResponseSettingsFromConfig(config: ActionsConfig): ResponseSettings { + return { + maxContentLength: config.maxResponseContentLength.getValueInBytes(), + timeout: config.responseTimeout.asMilliseconds(), + }; +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { @@ -110,6 +118,7 @@ export function getActionsConfigurationUtilities( isUriAllowed, isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), + getResponseSettings: () => getResponseSettingsFromConfig(config), isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index b858d5491a6bd..4596619c50940 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -283,6 +283,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -342,6 +343,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index a932b38ede2bb..edc9429e4fac6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -42,6 +42,10 @@ describe('request', () => { headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly with defaults', async () => { @@ -58,6 +62,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -88,6 +94,8 @@ describe('request', () => { httpAgent, httpsAgent, proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -116,6 +124,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -224,6 +234,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -235,10 +247,15 @@ describe('request', () => { describe('patch', () => { beforeEach(() => { + jest.resetAllMocks(); axiosMock.mockImplementation(() => ({ status: 200, headers: { 'content-type': 'application/json' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly', async () => { @@ -249,6 +266,8 @@ describe('patch', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index edce369096142..af353e1d1da5a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -31,6 +31,7 @@ export const request = async ({ auth?: AxiosBasicCredentials; }): Promise => { const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url); + const { maxContentLength, timeout } = configurationUtilities.getResponseSettings(); return await axios(url, { ...rest, @@ -40,6 +41,8 @@ export const request = async ({ httpAgent, httpsAgent, proxy: false, + maxContentLength, + timeout, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index c31adddc5a57e..8a185d353de02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -168,6 +168,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -230,6 +231,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index c468453247809..d3f059eede615 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -291,6 +291,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -329,6 +330,33 @@ describe('execute()', () => { `); }); + test('execute with exception maxContentLength size exceeded should log the proper error', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + hasAuth: true, + }; + requestMock.mockReset(); + requestMock.mockRejectedValueOnce({ + tag: 'err', + isAxiosError: true, + message: 'maxContentLength size of 1000000 exceeded', + }); + await actionType.executor({ + actionId: 'some-id', + services, + config, + secrets: { user: 'abc', password: '123' }, + params: { body: 'some data' }, + }); + expect(mockedLogger.error).toBeCalledWith( + 'error on some-id webhook event: maxContentLength size of 1000000 exceeded' + ); + }); + test('execute without username/password sends request without basic auth', async () => { const config: ActionTypeConfigType = { url: 'https://abc.def/my-webhook', @@ -355,6 +383,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 269449686acf0..93c9bbdbab18a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -180,7 +180,6 @@ export async function executor( return successResult(actionId, data); } else { const { error } = result; - if (error.response) { const { status, @@ -211,6 +210,10 @@ export async function executor( const message = `[${error.code}] ${error.message}`; logger.error(`error on ${actionId} webhook event: ${message}`); return errorResultRequestFailed(actionId, message); + } else if (error.isAxiosError) { + const message = `${error.message}`; + logger.error(`error on ${actionId} webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); } logger.error(`error on ${actionId} webhook action: unexpected error`); diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 0d270512d1dee..2eecaa19da0c5 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -27,9 +27,13 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object {}, "proxyRejectUnauthorizedCertificates": true, "rejectUnauthorized": true, + "responseTimeout": "PT1M", } `); }); @@ -57,6 +61,9 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object { "mySlack1": Object { "actionTypeId": ".slack", @@ -69,6 +76,7 @@ describe('config validation', () => { }, "proxyRejectUnauthorizedCertificates": false, "rejectUnauthorized": false, + "responseTimeout": "PT1M", } `); }); diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 450f03308ab0b..4aa77ded315b8 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -47,6 +47,8 @@ export const configSchema = schema.object({ proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), rejectUnauthorized: schema.boolean({ defaultValue: true }), + maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), + responseTimeout: schema.duration({ defaultValue: '60s' }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index b8f83e91239e2..30bbedbedbe9c 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; @@ -37,6 +39,8 @@ describe('Actions Plugin', () => { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -197,6 +201,8 @@ describe('Actions Plugin', () => { }, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 6830f013ade5f..b7a6750a520ea 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -138,3 +138,8 @@ export interface ProxySettings { proxyHeaders?: Record; proxyRejectUnauthorizedCertificates: boolean; } + +export interface ResponseSettings { + maxContentLength: number; + timeout: number; +} From fc9f97e03bf4ad32493565e0a49bbb564fd057e5 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 16:04:13 -0700 Subject: [PATCH 093/131] skip suites failing es promotion (#96515) (cherry picked from commit 7fdf7e1d7913c3f5ab5af1388d02a8a880702999) --- x-pack/test/fleet_api_integration/apis/epm/list.ts | 3 ++- .../security_solution_endpoint_api_int/apis/artifacts/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/epm/list.ts b/x-pack/test/fleet_api_integration/apis/epm/list.ts index 5a991e52bdba4..0a7002764a54c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/list.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/list.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - describe('EPM - list', async function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('EPM - list', async function () { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('fleet/empty_fleet_server'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index e1edeb7808697..8ee028ae3f56b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - describe('artifact download', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('artifact download', () => { const esArchiverSnapshots = [ 'endpoint/artifacts/fleet_artifacts', 'endpoint/artifacts/api_feature', From a6b2a8477534b67d840f3569cd08d80ee9738dff Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 7 Apr 2021 18:33:18 -0500 Subject: [PATCH 094/131] fix index pattern field editor console error (#96497) --- .../public/components/field_editor_flyout_content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index e0ca654c956c6..13830f9233b5e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -143,7 +143,7 @@ const FieldEditorFlyoutContentComponent = ({ const [isValidating, setIsValidating] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - const [confirmContent, setConfirmContent] = useState(); + const [confirmContent, setConfirmContent] = useState(''); const { submit, isValid: isFormValid, isSubmitted } = formState; const { fields } = indexPattern; From d63fbb19cd9c16a4f56bb6499789c77e25cd496c Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 7 Apr 2021 18:57:24 -0600 Subject: [PATCH 095/131] [Security Solution][Detections]Fixes Rule Management Cypress Tests (#96505) ## Summary Fixes two cypress tests: > Deleting prebuilt rules "before each" hook for "Does not allow to delete one rule when more than one is selected" https://github.com/elastic/kibana/issues/68607 This one is more of a drive around the pot-hole fix as we were waiting for the Alerts Table to load when we really didn't need to. Removed unnecessary check.

> Alerts rules, prebuilt rules Loads prebuilt rules https://github.com/elastic/kibana/issues/71300 This one was fixed with a `.pipe()` and `.should('not.be.visible')` to ensure the click was successful. Also removed unnecessary check on the Alerts Table loading that was present here as well too..

--- .../integration/detection_rules/prebuilt_rules.spec.ts | 8 +------- .../cypress/tasks/alerts_detection_rules.ts | 5 ++++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index d290773d425e2..fb0a01bd1c7d3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -14,11 +14,7 @@ import { SHOWING_RULES_TEXT, } from '../../screens/alerts_detection_rules'; -import { - goToManageAlertsDetectionRules, - waitForAlertsIndexToBeCreated, - waitForAlertsPanelToBeLoaded, -} from '../../tasks/alerts'; +import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { changeRowsPerPageTo300, deleteFirstRule, @@ -47,7 +43,6 @@ describe('Alerts rules, prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); @@ -79,7 +74,6 @@ describe('Deleting prebuilt rules', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 10644e046a68b..d66b839267ea0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -191,7 +191,10 @@ export const resetAllRulesIdleModalTimeout = () => { export const changeRowsPerPageTo = (rowsCount: number) => { cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); - cy.get(rowsPerPageSelector(rowsCount)).click(); + cy.get(rowsPerPageSelector(rowsCount)) + .pipe(($el) => $el.trigger('click')) + .should('not.be.visible'); + waitForRulesTableToBeRefreshed(); }; From 0cf31ae22fc732255ee0e2ef159470e8a2497315 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 18:57:40 -0700 Subject: [PATCH 096/131] skip suite block es promotion (#96515) (cherry picked from commit f06be93a406878d085e75f8351ca48f41fd37779) --- x-pack/test/fleet_api_integration/apis/fleet_setup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index c9709475d182d..a82ed3f8cf22d 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -15,7 +15,8 @@ export default function (providerContext: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - describe('fleet_setup', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('fleet_setup', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('empty_kibana'); From d5b3829210c5d502b8cfa07ffcbb978c572f83b9 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 7 Apr 2021 22:25:47 -0600 Subject: [PATCH 097/131] [Security Solution][Detections] Fixes Closing Alerts Cypress Test (#96523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary As identified in https://github.com/elastic/kibana/pull/96505#issuecomment-815392671, this fixes the flakiness in the `Closing alerts` cypress test. Method used was to just delete the rule after the initial batch of alerts were generated. Alternatively we could add a function for disabling the rule (didn't see one in there), but the outcome is the same, no more alerts generated while the test is being performed. 🙂 > Passing locally, though upon further inspection, this test is definitely going to be flakey as it's checking counts on alerts as they move through different states and there are new alerts that keep coming in (hence the count mis-match in the above failure). Potential fixes would be to use an absolute daterange to after the first round of alerts were generated, or just stop generating alerts before performing the alert state changes. ##### Before:

##### After:

--- .../cypress/integration/detection_alerts/closing.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index e9d17a361d336..b7c0e1c6fcd6e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -25,7 +25,7 @@ import { waitForAlerts, waitForAlertsIndexToBeCreated, } from '../../tasks/alerts'; -import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; +import { createCustomRuleActivated, deleteCustomRule } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -42,6 +42,7 @@ describe('Closing alerts', () => { createCustomRuleActivated(newRule); refreshPage(); waitForAlertsToPopulate(); + deleteCustomRule(); }); it('Closes and opens alerts', () => { From 93965343e5565dc74282bb5216a8605ac7b00f1d Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Wed, 7 Apr 2021 22:52:10 -0600 Subject: [PATCH 098/131] [Fleet] Install security_rule assets as saved objects (#95885) * [Fleet] Install security_rule assets as saved objects * Add security-rule to update_assets.ts * Update UUIDs for security_rule asset * Change .type to match the saved object type not the asset type * Add saved object mapping for security-rule * Make SO non-hidden * Fix SO mapping for security-rule * Make security-rule a non-hidden asset --- .../package_to_package_policy.test.ts | 1 + .../plugins/fleet/common/types/models/epm.ts | 2 + .../fleet/sections/epm/constants.tsx | 2 + .../services/epm/kibana/assets/install.ts | 2 + .../services/epm/packages/assets.test.ts | 2 +- .../rules/saved_object_mappings.ts | 24 +++++++++ .../security_solution/server/saved_objects.ts | 6 ++- .../apis/epm/install_remove_assets.ts | 10 ++++ .../apis/epm/update_assets.ts | 5 ++ .../security_rule/sample_security_rule.json | 50 +++++++++++++++++++ .../security_rule/sample_security_rule.json | 50 +++++++++++++++++++ 11 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index a4cca4455a274..65b853ed5b38f 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -31,6 +31,7 @@ describe('Fleet - packageToPackagePolicy', () => { map: [], lens: [], ml_module: [], + security_rule: [], }, elasticsearch: { ingest_pipeline: [], diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 80fabd51613ae..3bc0d97d64646 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -50,6 +50,7 @@ export enum KibanaAssetType { indexPattern = 'index_pattern', map = 'map', lens = 'lens', + securityRule = 'security_rule', mlModule = 'ml_module', } @@ -64,6 +65,7 @@ export enum KibanaSavedObjectType { map = 'map', lens = 'lens', mlModule = 'ml-module', + securityRule = 'security-rule', } export enum ElasticsearchAssetType { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx index ea19a330adfee..6ddff968bd3f3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx @@ -33,6 +33,7 @@ export const AssetTitleMap: Record = { map: 'Map', data_stream_ilm_policy: 'Data Stream ILM Policy', lens: 'Lens', + security_rule: 'Security Rule', ml_module: 'ML Module', }; @@ -48,6 +49,7 @@ export const AssetIcons: Record = { visualization: 'visualizeApp', map: 'emsApp', lens: 'lensApp', + security_rule: 'securityApp', ml_module: 'mlApp', }; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index bfcc40e18fe80..0f2d7b6679bf9 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -38,6 +38,7 @@ const KibanaSavedObjectTypeMapping: Record { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts index 999cf878d07b7..c5b104696aaf4 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts @@ -43,7 +43,7 @@ const tests = [ name: 'coredns', version: '1.0.1', }, - // Non existant dataset + // Non existent dataset dataset: 'foo', filter: (path: string) => { return true; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts index 4ed53e39fa5eb..813e800f34ce2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -53,3 +53,27 @@ export const type: SavedObjectsType = { namespaceType: 'single', mappings: ruleStatusSavedObjectMappings, }; + +export const ruleAssetSavedObjectType = 'security-rule'; + +export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + rule_id: { + type: 'keyword', + }, + version: { + type: 'long', + }, + }, +}; + +export const ruleAssetType: SavedObjectsType = { + name: ruleAssetSavedObjectType, + hidden: false, + namespaceType: 'agnostic', + mappings: ruleAssetSavedObjectMappings, +}; diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index d483bd25266af..42abb3dab2ac4 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -8,7 +8,10 @@ import { CoreSetup } from '../../../../src/core/server'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; -import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings'; +import { + type as ruleStatusType, + ruleAssetType, +} from './lib/detection_engine/rules/saved_object_mappings'; import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { @@ -21,6 +24,7 @@ const types = [ pinnedEventType, ruleActionsType, ruleStatusType, + ruleAssetType, timelineType, exceptionsArtifactType, manifestType, diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index abc91a973e6b6..8e09e331bf867 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -399,6 +399,11 @@ const expectAssetsInstalled = ({ id: 'sample_ml_module', }); expect(resMlModule.id).equal('sample_ml_module'); + const resSecurityRule = await kibanaServer.savedObjects.get({ + type: 'security-rule', + id: 'sample_security_rule', + }); + expect(resSecurityRule.id).equal('sample_security_rule'); const resIndexPattern = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'test-*', @@ -472,6 +477,10 @@ const expectAssetsInstalled = ({ id: 'sample_search', type: 'search', }, + { + id: 'sample_security_rule', + type: 'security-rule', + }, { id: 'sample_visualization', type: 'visualization', @@ -537,6 +546,7 @@ const expectAssetsInstalled = ({ { id: 'e21b59b5-eb76-5ab0-bef2-1c8e379e6197', type: 'epm-packages-assets' }, { id: '4c758d70-ecf1-56b3-b704-6d8374841b34', type: 'epm-packages-assets' }, { id: 'e786cbd9-0f3b-5a0b-82a6-db25145ebf58', type: 'epm-packages-assets' }, + { id: 'd8b175c3-0d42-5ec7-90c1-d1e4b307a4c2', type: 'epm-packages-assets' }, { id: '53c94591-aa33-591d-8200-cd524c2a0561', type: 'epm-packages-assets' }, { id: 'b658d2d4-752e-54b8-afc2-4c76155c1466', type: 'epm-packages-assets' }, ], diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 1a559ac5a5c75..9b55822311bd7 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -296,6 +296,10 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_lens', type: 'lens', }, + { + id: 'sample_security_rule', + type: 'security-rule', + }, { id: 'sample_ml_module', type: 'ml-module', @@ -350,6 +354,7 @@ export default function (providerContext: FtrProviderContext) { { id: '7f4c5aca-b4f5-5f0a-95af-051da37513fc', type: 'epm-packages-assets' }, { id: '4281a436-45a8-54ab-9724-fda6849f789d', type: 'epm-packages-assets' }, { id: '2e56f08b-1d06-55ed-abee-4708e1ccf0aa', type: 'epm-packages-assets' }, + { id: '4035007b-9c33-5227-9803-2de8a17523b5', type: 'epm-packages-assets' }, { id: 'c7bf1a39-e057-58a0-afde-fb4b48751d8c', type: 'epm-packages-assets' }, { id: '8c665f28-a439-5f43-b5fd-8fda7b576735', type: 'epm-packages-assets' }, ], diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json new file mode 100644 index 0000000000000..6bedde67b8923 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json @@ -0,0 +1,50 @@ +{ + "attributes": { + "author": [ + "Elastic" + ], + "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "kuery", + "license": "Elastic License v2", + "name": "Svchost spawning Cmd", + "query": "event.category:process and event.type:(start or process_started) and process.parent.name:svchost.exe and process.name:cmd.exe", + "risk_score": 21, + "rule_id": "sample_security_rule", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT\u0026CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 7 + }, + "id": "sample_security_rule", + "type": "security-rule" +} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json new file mode 100644 index 0000000000000..6bedde67b8923 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json @@ -0,0 +1,50 @@ +{ + "attributes": { + "author": [ + "Elastic" + ], + "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "kuery", + "license": "Elastic License v2", + "name": "Svchost spawning Cmd", + "query": "event.category:process and event.type:(start or process_started) and process.parent.name:svchost.exe and process.name:cmd.exe", + "risk_score": 21, + "rule_id": "sample_security_rule", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT\u0026CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 7 + }, + "id": "sample_security_rule", + "type": "security-rule" +} From 391e92ead376a7681f91eeeb6fb5228b07f4e933 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 8 Apr 2021 07:16:23 +0200 Subject: [PATCH 099/131] [Exploratory view] Use index patterns for formatting (#96280) --- .github/CODEOWNERS | 1 + .../index_patterns/index_pattern.stub.ts | 6 + ...iscover_field_details_footer.test.tsx.snap | 1 + x-pack/plugins/lens/public/index.ts | 2 +- x-pack/plugins/lens/public/mocks.tsx | 2 +- .../{ => apm}/service_latency_config.ts | 10 +- .../{ => apm}/service_throughput_config.ts | 10 +- .../{ => constants}/constants.ts | 19 +-- .../elasticsearch_fieldnames.ts | 0 .../configurations/constants/index.ts | 8 ++ .../{ => constants}/url_constants.ts | 0 .../configurations/default_configs.ts | 20 +-- .../configurations/lens_attributes.test.ts | 15 +-- .../{ => logs}/logs_frequency_config.ts | 4 +- .../{ => metrics}/cpu_usage_config.ts | 8 +- .../{ => metrics}/memory_usage_config.ts | 8 +- .../{ => metrics}/network_activity_config.ts | 8 +- .../configurations/rum/field_formats.ts | 74 +++++++++++ .../{ => rum}/kpi_trends_config.ts | 8 +- .../{ => rum}/performance_dist_config.ts | 10 +- .../synthetics/field_formats.ts | 22 ++++ .../monitor_duration_config.ts | 8 +- .../{ => synthetics}/monitor_pings_config.ts | 4 +- .../{data => test_data}/sample_attribute.ts | 2 +- .../test_index_pattern.json | 0 .../exploratory_view/configurations/utils.ts | 4 +- .../exploratory_view.test.tsx | 2 +- .../exploratory_view/exploratory_view.tsx | 2 +- .../shared/exploratory_view/header/header.tsx | 2 +- .../hooks/use_default_index_pattern.tsx | 2 +- .../hooks/use_init_exploratory_view.ts | 13 +- .../hooks/use_lens_attributes.ts | 2 +- .../hooks/use_series_filters.ts | 2 +- ...e_url_strorage.tsx => use_url_storage.tsx} | 2 +- .../shared/exploratory_view/index.tsx | 2 +- .../shared/exploratory_view/rtl_helpers.tsx | 6 +- .../columns/data_types_col.test.tsx | 2 +- .../series_builder/columns/data_types_col.tsx | 2 +- .../columns/report_breakdowns.test.tsx | 4 +- .../columns/report_breakdowns.tsx | 2 +- .../columns/report_definition_col.test.tsx | 4 +- .../columns/report_definition_col.tsx | 3 +- .../columns/report_filters.test.tsx | 2 +- .../series_builder/columns/report_filters.tsx | 2 +- .../columns/report_types_col.tsx | 2 +- .../series_builder/custom_report_field.tsx | 2 +- .../series_builder/series_builder.tsx | 2 +- .../series_date_picker/index.tsx | 2 +- .../series_editor/columns/breakdowns.test.tsx | 4 +- .../series_editor/columns/breakdowns.tsx | 2 +- .../series_editor/columns/chart_types.tsx | 2 +- .../columns/filter_expanded.test.tsx | 2 +- .../series_editor/columns/filter_expanded.tsx | 2 +- .../columns/filter_value_btn.test.tsx | 2 +- .../columns/filter_value_btn.tsx | 2 +- .../columns/metric_selection.tsx | 6 +- .../series_editor/columns/remove_series.tsx | 2 +- .../series_editor/columns/series_filter.tsx | 4 +- .../series_editor/selected_filters.test.tsx | 4 +- .../series_editor/selected_filters.tsx | 2 +- .../series_editor/series_editor.tsx | 2 +- .../shared/exploratory_view/types.ts | 19 +++ .../observability_index_patterns.test.ts | 95 ++++++++++++++ .../utils/observability_index_patterns.ts | 124 ++++++++++++++++++ .../field_value_selection.tsx | 1 + 65 files changed, 470 insertions(+), 123 deletions(-) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => apm}/service_latency_config.ts (82%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => apm}/service_throughput_config.ts (82%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => constants}/constants.ts (80%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{data => constants}/elasticsearch_fieldnames.ts (100%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => constants}/url_constants.ts (100%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => logs}/logs_frequency_config.ts (90%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => metrics}/cpu_usage_config.ts (82%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => metrics}/memory_usage_config.ts (82%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => metrics}/network_activity_config.ts (81%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => rum}/kpi_trends_config.ts (90%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => rum}/performance_dist_config.ts (90%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => synthetics}/monitor_duration_config.ts (83%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{ => synthetics}/monitor_pings_config.ts (92%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{data => test_data}/sample_attribute.ts (98%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/{data => test_data}/test_index_pattern.json (100%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/{use_url_strorage.tsx => use_url_storage.tsx} (97%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a8dcafeb7753c..92e39c2e634e5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -79,6 +79,7 @@ # Uptime /x-pack/plugins/uptime @elastic/uptime +/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime /x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime /x-pack/test/functional/apps/uptime @elastic/uptime /x-pack/test/api_integration/apis/uptime @elastic/uptime diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index fa33f00a49879..36569cafd6611 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -9,6 +9,7 @@ import sinon from 'sinon'; import { CoreSetup } from 'src/core/public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldType, FieldSpec } from '../../common/index_patterns'; import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../'; import { getFieldFormatsRegistry } from '../test_utils'; @@ -51,6 +52,7 @@ export class StubIndexPattern { _reindexFields: Function; stubSetFieldFormat: Function; fields?: FieldSpec[]; + setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; constructor( pattern: string, @@ -74,6 +76,10 @@ export class StubIndexPattern { this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; + this.setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { + this.fieldFormatMap[fieldName] = format; + }; + this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = indexPatterns.flattenHitWrapper( (this as unknown) as IndexPattern, diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap index f3c8990388024..f976b961d8520 100644 --- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap @@ -543,6 +543,7 @@ exports[`discover sidebar field details footer renders properly 1`] = ` "_source", ], "popularizeField": [Function], + "setFieldFormat": [Function], "stubSetFieldFormat": [Function], "timeFieldName": "time", "title": "logstash-*", diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 9b53e59f96792..cedb648215c0e 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -21,7 +21,7 @@ export type { YAxisMode, XYCurveType, } from './xy_visualization/types'; -export type { DataType } from './types'; +export type { DataType, OperationMetadata } from './types'; export type { PieVisualizationState, PieLayerState, diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 743846d81213c..c1f885d167659 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -18,7 +18,7 @@ const createStartContract = (): Start => { }), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), - getXyVisTypes: jest.fn().mockReturnValue(new Promise(() => visualizationTypes)), + getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), }; return startContract; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts index a31679c61a4ab..3fcf98f712bef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; +import { OperationType } from '../../../../../../../lens/public'; export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -20,7 +20,7 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'transaction.duration.us', label: 'Latency', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts index 32cae2167ddf0..c0f3d6dc9b010 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants/constants'; +import { buildPhraseFilter } from '../utils'; +import { OperationType } from '../../../../../../../lens/public'; export function getServiceThroughputLensConfig({ seriesId, @@ -23,7 +23,7 @@ export function getServiceThroughputLensConfig({ sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'transaction.duration.us', label: 'Throughput', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts similarity index 80% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index aa3ac2fa64317..ed849c1eb47b3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,14 +5,8 @@ * 2.0. */ -import { AppDataType, ReportViewTypeId } from '../types'; -import { - CLS_FIELD, - FCP_FIELD, - FID_FIELD, - LCP_FIELD, - TBT_FIELD, -} from './data/elasticsearch_fieldnames'; +import { AppDataType, ReportViewTypeId } from '../../types'; +import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; export const FieldLabels: Record = { 'user_agent.name': 'Browser family', @@ -24,10 +18,10 @@ export const FieldLabels: Record = { 'service.name': 'Service Name', 'service.environment': 'Environment', - [LCP_FIELD]: 'Largest contentful paint', - [FCP_FIELD]: 'First contentful paint', - [TBT_FIELD]: 'Total blocking time', - [FID_FIELD]: 'First input delay', + [LCP_FIELD]: 'Largest contentful paint (Seconds)', + [FCP_FIELD]: 'First contentful paint (Seconds)', + [TBT_FIELD]: 'Total blocking time (Seconds)', + [FID_FIELD]: 'First input delay (Seconds)', [CLS_FIELD]: 'Cumulative layout shift', 'monitor.id': 'Monitor Id', @@ -38,6 +32,7 @@ export const FieldLabels: Record = { 'monitor.name': 'Monitor name', 'monitor.type': 'Monitor Type', 'url.port': 'Port', + 'url.full': 'Url', tags: 'Tags', // custom diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts new file mode 100644 index 0000000000000..63661f0d5a996 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './constants'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 85d48ef638d44..2c5b4ebea0ab3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -6,16 +6,16 @@ */ import { ReportViewTypes } from '../types'; -import { getPerformanceDistLensConfig } from './performance_dist_config'; -import { getMonitorDurationConfig } from './monitor_duration_config'; -import { getServiceLatencyLensConfig } from './service_latency_config'; -import { getMonitorPingsConfig } from './monitor_pings_config'; -import { getServiceThroughputLensConfig } from './service_throughput_config'; -import { getKPITrendsLensConfig } from './kpi_trends_config'; -import { getCPUUsageLensConfig } from './cpu_usage_config'; -import { getMemoryUsageLensConfig } from './memory_usage_config'; -import { getNetworkActivityLensConfig } from './network_activity_config'; -import { getLogsFrequencyLensConfig } from './logs_frequency_config'; +import { getPerformanceDistLensConfig } from './rum/performance_dist_config'; +import { getMonitorDurationConfig } from './synthetics/monitor_duration_config'; +import { getServiceLatencyLensConfig } from './apm/service_latency_config'; +import { getMonitorPingsConfig } from './synthetics/monitor_pings_config'; +import { getServiceThroughputLensConfig } from './apm/service_throughput_config'; +import { getKPITrendsLensConfig } from './rum/kpi_trends_config'; +import { getCPUUsageLensConfig } from './metrics/cpu_usage_config'; +import { getMemoryUsageLensConfig } from './metrics/memory_usage_config'; +import { getNetworkActivityLensConfig } from './metrics/network_activity_config'; +import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index dcfaed938cc0f..139f3ab0d82ed 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -8,9 +8,8 @@ import { LensAttributes } from './lens_attributes'; import { mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; -import { sampleAttribute } from './data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames'; -import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames'; +import { sampleAttribute } from './test_data/sample_attribute'; +import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; describe('Lens Attribute', () => { const reportViewConfig = getDefaultConfigs({ @@ -93,7 +92,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -129,7 +128,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getXAxis()).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -154,7 +153,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -318,7 +317,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -363,7 +362,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts index 68e5e697d2f9d..8a27d7ddd428b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts index 5a4fb2aa3a6a5..6214975d8f1dd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.cpu.user.pct', label: 'CPU Usage %', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts index 579372ed86fa7..6f46c175f7882 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.memory.used.pct', label: 'Memory Usage %', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts similarity index 81% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts index 63cdd0ec8bd60..1bc9fed9c3f80 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.memory.used.pct', }, hasMetricType: true, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts new file mode 100644 index 0000000000000..f1fc5f310b8ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; +import { + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; + +export const rumFieldFormats: FieldFormat[] = [ + { + field: TRANSACTION_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + showSuffix: true, + outputPrecision: 1, + }, + }, + }, + { + field: FCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: LCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: TBT_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: FID_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts index a967a8824bca7..a1a3acd51f89c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, PROCESSOR_EVENT, @@ -18,7 +18,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; +} from '../constants/elasticsearch_fieldnames'; export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index 41617304c9f3d..7005dea29d60d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, CLS_FIELD, @@ -24,7 +24,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; +} from '../constants/elasticsearch_fieldnames'; export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -80,7 +80,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application', - [TRANSACTION_DURATION]: 'Page load time', + [TRANSACTION_DURATION]: 'Page load time (Seconds)', }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts new file mode 100644 index 0000000000000..4f036f0b9be65 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; + +export const syntheticsFieldFormats: FieldFormat[] = [ + { + field: 'monitor.duration.us', + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asMilliseconds', + outputPrecision: 0, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts similarity index 83% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts index aa9b8b94c6d86..f0ec3f0c31bef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants/constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getMonitorDurationConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'monitor.duration.us', label: 'Monitor duration (ms)', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts index 72968626e934b..40c9f5750fb4d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts similarity index 98% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bcc..ffce81207472f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -21,7 +21,7 @@ export const sampleAttribute = { columns: { 'x-axis-column': { sourceField: 'transaction.duration.us', - label: 'Page load time', + label: 'Page load time (Seconds)', dataType: 'number', operationType: 'range', isBucketed: true, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 38b8ce81b2acd..c885673134786 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,11 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage'; +import type { AllSeries, AllShortSeries } from '../hooks/use_url_storage'; import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { URL_KEYS } from './url_constants'; +import { URL_KEYS } from './constants/url_constants'; export function convertToShortUrl(series: SeriesUrl) { const { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index b90d5115bc41e..257eb3a739f0f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -10,7 +10,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { render, mockUrlStorage, mockCore } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; -import * as obsvInd from '../../../utils/observability_index_patterns'; +import * as obsvInd from './utils/observability_index_patterns'; describe('ExploratoryView', () => { beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index b3ad107bbe0e2..0e7bc80e8659c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; import { SeriesEditor } from './series_editor/series_editor'; -import { useUrlStorage } from './hooks/use_url_strorage'; +import { useUrlStorage } from './hooks/use_url_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { EmptyView } from './components/empty_view'; import { useIndexPatternContext } from './hooks/use_default_index_pattern'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bda3566c76602..17f06436c8535 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -12,7 +12,7 @@ import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { DataViewLabels } from '../configurations/constants'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx index 04cbb4a4ddb18..7ead7d5e3cfad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx @@ -10,7 +10,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { AppDataType } from '../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns'; +import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export interface IIndexPatternContext { indexPattern: IndexPattern; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts index 9f462790e8d37..76fd64ef86736 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts @@ -8,12 +8,9 @@ import { useFetcher } from '../../../..'; import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { AllShortSeries } from './use_url_strorage'; +import { AllShortSeries } from './use_url_storage'; import { ReportToDataTypeMap } from '../configurations/constants'; -import { - DataType, - ObservabilityIndexPatterns, -} from '../../../../utils/observability_index_patterns'; +import { DataType, ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const { @@ -30,7 +27,7 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const firstSeries = allSeries[firstSeriesId]; - const { data: indexPattern } = useFetcher(() => { + const { data: indexPattern, error } = useFetcher(() => { const obsvIndexP = new ObservabilityIndexPatterns(data); let reportType: DataType = 'apm'; if (firstSeries?.rt) { @@ -40,5 +37,9 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { return obsvIndexP.getIndexPattern(reportType); }, [firstSeries?.rt, data]); + if (error) { + throw error; + } + return indexPattern; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c735009f66f9..274542380c137 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,7 +8,7 @@ import { useMemo } from 'react'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LensAttributes } from '../configurations/lens_attributes'; -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 35247180c2ee5..34f0a7c1a7f86 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { UrlFilter } from '../types'; export interface UpdateFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx similarity index 97% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx index d38429703b709..6256b3b134f8c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx @@ -10,7 +10,7 @@ import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_ import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types'; import { convertToShortUrl } from '../configurations/utils'; import { OperationType, SeriesType } from '../../../../../../lens/public'; -import { URL_KEYS } from '../configurations/url_constants'; +import { URL_KEYS } from '../configurations/constants/url_constants'; export const UrlStorageContext = createContext(null); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index dc47a0f075fe6..f903c4d7d44fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -18,7 +18,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../../../src/plugins/kibana_utils/public/'; -import { UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { UrlStorageContextProvider } from './hooks/use_url_storage'; import { useInitExploratoryView } from './hooks/use_init_exploratory_view'; import { WithHeaderLayout } from '../../app/layout/with_header'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 112bfcc3ccb58..b826409dd9e3a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -23,20 +23,20 @@ import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { lensPluginMock } from '../../../../../lens/public/mocks'; import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; -import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_storage'; import { withNotifyOnErrors, createKbnUrlStateStorage, } from '../../../../../../../src/plugins/kibana_utils/public'; import * as fetcherHook from '../../../hooks/use_fetcher'; -import * as useUrlHook from './hooks/use_url_strorage'; +import * as useUrlHook from './hooks/use_url_storage'; import * as useSeriesFilterHook from './hooks/use_series_filters'; import * as useHasDataHook from '../../../hooks/use_has_data'; import * as useValuesListHook from '../../../hooks/use_values_list'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub'; -import indexPatternData from './configurations/data/test_index_pattern.json'; +import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index d33d8515d3bee..039cdfc9b73f5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { mockUrlStorage, render } from '../../rtl_helpers'; import { dataTypes, DataTypesCol } from './data_types_col'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; describe('DataTypesCol', function () { it('should render properly', function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 7ea44e66a721a..b6464bbe3c6ed 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AppDataType } from '../../types'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ { id: 'synthetics', label: 'Synthetic Monitoring' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index dba660fff9c36..553aff57ad491 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -10,9 +10,9 @@ import { fireEvent, screen } from '@testing-library/react'; import { render } from '../../../../../utils/test_helper'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportBreakdowns } from './report_breakdowns'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportBreakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx index 7667cea417a52..619e2ec4fe9b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { DataSeries } from '../../types'; export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 2fda581154166..104a8fcefb49f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportDefinitionCol } from './report_definition_col'; -import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index ce11c869de0ab..b907efb57d5c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; import { CustomReportField } from '../custom_report_field'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { DataSeries } from '../../types'; @@ -67,6 +67,7 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe {rtd?.[field] && ( ; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx index 6039fd4cba280..4d5033eca241b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { ReportDefinition } from '../types'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index 983c18af031d0..053f301529635 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -16,7 +16,7 @@ import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; import { ReportFilters } from './columns/report_filters'; import { ReportBreakdowns } from './columns/report_breakdowns'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx index 71e3317ad6db8..922d33ffd39ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -8,7 +8,7 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHasData } from '../../../../hooks/use_has_data'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; export interface TimePickerTime { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 654a93a08a7c8..0824f13e6b3fe 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { getDefaultConfigs } from '../../configurations/default_configs'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 0d34d7245725a..5561779daa8c4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldLabels } from '../../configurations/constants'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx index 017655053eef2..f83630cff414a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; import { useFetcher } from '../../../../..'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { SeriesType } from '../../../../../../../lens/public'; export function SeriesChartTypes({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index edd5546f13940..530b8dee3a4d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 280912dd0902f..3e6d7890f4c81 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -14,7 +14,7 @@ import { EuiFilterGroup, } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 7f76c9ea999ee..befbb3b74d6d7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -12,7 +12,7 @@ import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from ' import { USER_AGENT_NAME, USER_AGENT_VERSION, -} from '../../configurations/data/elasticsearch_fieldnames'; +} from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterValueButton', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 42cdfd595e66b..efccb351c2619 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx index e01e371b5eeeb..fa4202d2c30ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx @@ -8,12 +8,12 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { OperationType } from '../../../../../../../lens/public'; const toggleButtons = [ { - id: `avg`, + id: `average`, label: i18n.translate('xpack.observability.expView.metricsSelect.average', { defaultMessage: 'Average', }), @@ -49,7 +49,7 @@ export function MetricSelection({ const [isOpen, setIsOpen] = useState(false); - const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg'); + const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'average'); const onChange = (optionId: OperationType) => { setToggleIdSelected(optionId); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx index 67aebed943326..aaaa02c7c5697 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { DataSeries } from '../../types'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { series: DataSeries; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index 24b65d2adb38e..c9bb44cfd8cca 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -17,9 +17,9 @@ import { } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; import { DataSeries } from '../../types'; -import { FieldLabels } from '../../configurations/constants'; +import { FieldLabels } from '../../configurations/constants/constants'; import { SelectedFilters } from '../selected_filters'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 5770a7e209f06..a38b50d610c75 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -10,8 +10,8 @@ import { screen, waitFor } from '@testing-library/react'; import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { NEW_SERIES_KEY } from '../hooks/use_url_strorage'; -import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames'; +import { NEW_SERIES_KEY } from '../hooks/use_url_storage'; +import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; describe('SelectedFilters', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index be8b1feb4d723..34e69f688eaaf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { FilterLabel } from '../components/filter_label'; import { DataSeries, UrlFilter } from '../types'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 2d423c9aee3fc..2d8bd12904fbd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -13,7 +13,7 @@ import { ActionsCol } from './columns/actions_col'; import { Breakdowns } from './columns/breakdowns'; import { DataSeries } from '../types'; import { SeriesBuilder } from '../series_builder/series_builder'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DatePickerCol } from './columns/date_picker_col'; import { RemoveSeries } from './columns/remove_series'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 444e0ddaecb4a..d673fc4d6f6ee 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -87,3 +87,22 @@ export interface ConfigProps { } export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; + +type FormatType = 'duration' | 'number'; +type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; +type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize'; + +export interface FieldFormatParams { + inputFormat: InputFormat; + outputFormat: OutputFormat; + outputPrecision?: number; + showSuffix?: boolean; +} + +export interface FieldFormat { + field: string; + format: { + id: FormatType; + params: FieldFormatParams; + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts new file mode 100644 index 0000000000000..b6f544db2a319 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { indexPatternList, ObservabilityIndexPatterns } from './observability_index_patterns'; +import { mockCore, mockIndexPattern } from '../rtl_helpers'; +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; + +const fieldFormats = { + 'transaction.duration.us': { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + outputPrecision: 1, + showSuffix: true, + }, + }, + 'transaction.experience.fid': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.experience.tbt': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.firstContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.largestContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, +}; + +describe('ObservabilityIndexPatterns', function () { + const { data } = mockCore(); + data!.indexPatterns.get = jest.fn().mockReturnValue({ title: 'index-*' }); + data!.indexPatterns.createAndSave = jest.fn().mockReturnValue({ id: indexPatternList.rum }); + data!.indexPatterns.updateSavedObject = jest.fn(); + + it('should return index pattern for app', async function () { + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ title: 'index-*' }); + + expect(data?.indexPatterns.get).toHaveBeenCalledWith(indexPatternList.rum); + expect(data?.indexPatterns.get).toHaveBeenCalledTimes(1); + }); + + it('should creates missing index pattern', async function () { + data!.indexPatterns.get = jest.fn().mockImplementation(() => { + throw new SavedObjectNotFound('index_pattern'); + }); + + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ id: indexPatternList.rum }); + + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledWith({ + fieldFormats, + id: 'rum_static_index_pattern_id', + timeFieldName: '@timestamp', + title: '(rum-data-view)*,apm-*', + }); + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledTimes(1); + }); + + it('should return getFieldFormats', function () { + const obsv = new ObservabilityIndexPatterns(data!); + + expect(obsv.getFieldFormats('rum')).toEqual(fieldFormats); + }); + + it('should validate field formats', async function () { + mockIndexPattern.getFormatterForField = jest.fn().mockReturnValue({ params: () => {} }); + + const obsv = new ObservabilityIndexPatterns(data!); + + await obsv.validateFieldFormats('rum', mockIndexPattern); + + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledTimes(1); + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledWith( + expect.objectContaining({ fieldFormatMap: fieldFormats }) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts new file mode 100644 index 0000000000000..e0a2941b24d3c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { + DataPublicPluginStart, + IndexPattern, + FieldFormat as IFieldFormat, + IndexPatternSpec, +} from '../../../../../../../../src/plugins/data/public'; +import { rumFieldFormats } from '../configurations/rum/field_formats'; +import { syntheticsFieldFormats } from '../configurations/synthetics/field_formats'; +import { FieldFormat, FieldFormatParams } from '../types'; + +const appFieldFormats: Record = { + rum: rumFieldFormats, + apm: null, + logs: null, + metrics: null, + synthetics: syntheticsFieldFormats, +}; + +function getFieldFormatsForApp(app: DataType) { + return appFieldFormats[app]; +} + +export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum'; + +export const indexPatternList: Record = { + synthetics: 'synthetics_static_index_pattern_id', + apm: 'apm_static_index_pattern_id', + rum: 'rum_static_index_pattern_id', + logs: 'logs_static_index_pattern_id', + metrics: 'metrics_static_index_pattern_id', +}; + +const appToPatternMap: Record = { + synthetics: '(synthetics-data-view)*,heartbeat-*,synthetics-*', + apm: 'apm-*', + rum: '(rum-data-view)*,apm-*', + logs: 'logs-*,filebeat-*', + metrics: 'metrics-*,metricbeat-*', +}; + +export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) { + return ( + param1?.inputFormat === param2?.inputFormat && + param1?.outputFormat === param2?.outputFormat && + param1?.showSuffix === param2?.showSuffix && + param2?.outputPrecision === param1?.outputPrecision + ); +} + +export class ObservabilityIndexPatterns { + data?: DataPublicPluginStart; + + constructor(data: DataPublicPluginStart) { + this.data = data; + } + + async createIndexPattern(app: DataType) { + if (!this.data) { + throw new Error('data is not defined'); + } + + const pattern = appToPatternMap[app]; + + return await this.data.indexPatterns.createAndSave({ + title: pattern, + id: indexPatternList[app], + timeFieldName: '@timestamp', + fieldFormats: this.getFieldFormats(app), + }); + } + // we want to make sure field formats remain same + async validateFieldFormats(app: DataType, indexPattern: IndexPattern) { + const defaultFieldFormats = getFieldFormatsForApp(app); + if (defaultFieldFormats && defaultFieldFormats.length > 0) { + let isParamsDifferent = false; + defaultFieldFormats.forEach(({ field, format }) => { + const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!); + const params = fieldFormat.params(); + if (!isParamsSame(params, format.params)) { + indexPattern.setFieldFormat(field, format); + isParamsDifferent = true; + } + }); + if (isParamsDifferent) { + await this.data?.indexPatterns.updateSavedObject(indexPattern); + } + } + } + + getFieldFormats(app: DataType) { + const fieldFormatMap: IndexPatternSpec['fieldFormats'] = {}; + + (appFieldFormats?.[app] ?? []).forEach(({ field, format }) => { + fieldFormatMap[field] = format; + }); + + return fieldFormatMap; + } + + async getIndexPattern(app: DataType): Promise { + if (!this.data) { + throw new Error('data is not defined'); + } + try { + const indexPattern = await this.data?.indexPatterns.get(indexPatternList[app]); + + // this is intentional a non blocking call, so no await clause + this.validateFieldFormats(app, indexPattern); + return indexPattern; + } catch (e: unknown) { + if (e instanceof SavedObjectNotFound) { + return await this.createIndexPattern(app || 'apm'); + } + } + } +} diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index a44aab2da85be..d14039ba173ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -76,6 +76,7 @@ export function FieldValueSelection({ Date: Thu, 8 Apr 2021 00:53:02 -0500 Subject: [PATCH 100/131] [Detection Rules] Resolves regression where Elastic Endgame rules would warn about unmapped timestamp override field (#96394) related to https://github.com/elastic/detection-rules/pull/1082 ## Summary Endgame promotion rules in Kibana/7.12 are at version 5 and have timestamp_override defined (which should not be). These same rules are at version 4 in the detection-rules repo 7.12 branch and kibana/master and timestamp_override is not defined. These updates are targeted for 7.12.1 There most likely was an issue with the maze of backports and interlaced updates. To fix the rules, they need to be reconciled across: detection-rules 7.12 & main kibana 7.12.1 and master bump detection-rules/7.12 to v6 -> PR to kibana/master -> backport to 7.12.1 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../prepackaged_rules/endgame_adversary_behavior_detected.json | 2 +- .../rules/prepackaged_rules/endgame_cred_dumping_detected.json | 2 +- .../rules/prepackaged_rules/endgame_cred_dumping_prevented.json | 2 +- .../prepackaged_rules/endgame_cred_manipulation_detected.json | 2 +- .../prepackaged_rules/endgame_cred_manipulation_prevented.json | 2 +- .../rules/prepackaged_rules/endgame_exploit_detected.json | 2 +- .../rules/prepackaged_rules/endgame_exploit_prevented.json | 2 +- .../rules/prepackaged_rules/endgame_malware_detected.json | 2 +- .../rules/prepackaged_rules/endgame_malware_prevented.json | 2 +- .../prepackaged_rules/endgame_permission_theft_detected.json | 2 +- .../prepackaged_rules/endgame_permission_theft_prevented.json | 2 +- .../prepackaged_rules/endgame_process_injection_detected.json | 2 +- .../prepackaged_rules/endgame_process_injection_prevented.json | 2 +- .../rules/prepackaged_rules/endgame_ransomware_detected.json | 2 +- .../rules/prepackaged_rules/endgame_ransomware_prevented.json | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json index e3dcd03d9cd8b..bf53625cef750 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json index a1cde4af47028..43cb19f50d675 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json index 5be8ce5ae1ce1..29b5bc3f39cf1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json index 9205ad9a0a028..393591a241114 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json index af8e9640dbba7..e9ca199c4a791 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json index cb955134a9cef..a169582c2da92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json index b306dd0109568..b781a1fae1847 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json index 7519ef1be013c..f7a064961f039 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json index 47224626698c8..59cbd98e2d42b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json index ea3b0be99e70e..b3db96d6d121b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json index a9eccf31f2972..18b316a293da8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json index 13a9fc457fe42..861daa2d004c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json index 8d1254eaa0271..5f78a3517e931 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json index 4ef637205c008..4c060bb52f32f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json index 718d4728be675..78845ffc4c845 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } From f9317281d1ebdb8fb2aadf9f6a716cd776a51299 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 8 Apr 2021 02:09:29 -0700 Subject: [PATCH 101/131] skip suite blocking es promotion (#96515) --- x-pack/test/fleet_api_integration/apis/agents_setup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts index 91d6ca0119d1d..d49bc91251b01 100644 --- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts @@ -15,7 +15,8 @@ export default function (providerContext: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - describe('fleet_agents_setup', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('fleet_agents_setup', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('empty_kibana'); From 81ba969cfaf3590287a0b35d4729bf29ff4bab09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 8 Apr 2021 05:24:16 -0400 Subject: [PATCH 102/131] Skip rendering empty add action variables button as disabled (#96342) * Skip rendering empty add action variables button * Fix jest tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/add_message_variables.test.tsx | 12 ++++++++++++ .../application/components/add_message_variables.tsx | 7 +++++-- .../es_index/es_index_params.test.tsx | 7 +++++++ .../pagerduty/pagerduty_params.test.tsx | 7 +++++++ .../webhook/webhook_params.test.tsx | 7 +++++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx index 8d27edd9e4bcc..4e03a2a09bed4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx @@ -117,4 +117,16 @@ describe('AddMessageVariables', () => { wrapper.find('button[data-test-subj="variableMenuButton-deprecatedVar"]').getDOMNode() ).toBeDisabled(); }); + + test(`it does't render when no variables exist`, () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="fooAddVariableButton"]')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index bf89e4f6ae6e1..57b251fba0d45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -61,13 +61,16 @@ export const AddMessageVariables: React.FunctionComponent = ({ } ); + if ((messageVariables?.length ?? 0) === 0) { + return ; + } + return ( setIsVariablesPopoverOpen(true)} iconType="indexOpen" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 97c1c41f68730..b792cf6574455 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -22,6 +22,13 @@ describe('IndexParamsFields renders', () => { errors={{ index: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 6e3f4213b7907..4d47cbf3685a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -30,6 +30,13 @@ describe('PagerDutyParamsFields renders', () => { errors={{ summary: [], timestamp: [], dedupKey: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx index 801d9a6b43ec6..a3756ae74fd14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -21,6 +21,13 @@ describe('WebhookParamsFields renders', () => { errors={{ body: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); From d39701fc97139b057ba8854f0b79c0ac63fc416b Mon Sep 17 00:00:00 2001 From: hardikpnsp Date: Thu, 8 Apr 2021 16:07:01 +0530 Subject: [PATCH 103/131] [Telemetry] enforce import export type (#96487) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-analytics/tsconfig.json | 1 + packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +++- packages/kbn-telemetry-tools/tsconfig.json | 3 ++- src/plugins/kibana_usage_collection/tsconfig.json | 3 ++- .../telemetry/common/telemetry_config/index.ts | 6 ++---- src/plugins/telemetry/public/index.ts | 2 +- src/plugins/telemetry/server/index.ts | 9 ++++++--- .../telemetry_collection/get_data_telemetry/index.ts | 9 ++------- .../telemetry/server/telemetry_collection/index.ts | 11 ++++------- .../telemetry/server/telemetry_repository/index.ts | 2 +- src/plugins/telemetry/tsconfig.json | 3 ++- .../telemetry_collection_manager/server/index.ts | 2 +- .../telemetry_collection_manager/tsconfig.json | 3 ++- .../telemetry_management_section/public/index.ts | 2 +- .../telemetry_management_section/tsconfig.json | 3 ++- src/plugins/usage_collection/public/index.ts | 2 +- .../usage_collection/server/collector/index.ts | 10 ++++++---- src/plugins/usage_collection/server/index.ts | 7 +++---- .../usage_collection/server/usage_collection.mock.ts | 3 ++- src/plugins/usage_collection/tsconfig.json | 3 ++- .../telemetry_collection_xpack/server/index.ts | 2 +- .../server/telemetry_collection/index.ts | 2 +- .../plugins/telemetry_collection_xpack/tsconfig.json | 3 ++- 23 files changed, 50 insertions(+), 45 deletions(-) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index c2e579e7fdbea..80a2255d71805 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,6 +7,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, + "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index 5d946b73d9759..f55a9aa80d40d 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,7 +7,9 @@ */ export { ErrorReporter } from './error_reporter'; -export { TaskContext, createTaskContext } from './task_context'; + +export type { TaskContext } from './task_context'; +export { createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 39946fe9907e5..419af1d02f83b 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,7 +6,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", + "isolatedModules": true }, "include": [ "src/**/*", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index d664d936f6667..ee07dfe589e4a 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/*", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index 84b6486f35b24..cc4ff102742d7 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,7 +9,5 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { - getTelemetryFailureDetails, - TelemetryFailureDetails, -} from './get_telemetry_failure_details'; +export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; +export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 6cca9bdf881dd..47ba7828eaec2 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index debdf7515cd58..1c335426ffd03 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,9 +34,12 @@ export { constants }; export { getClusterUuids, getLocalStats, - TelemetryLocalStats, DATA_TELEMETRY_ID, + buildDataTelemetryPayload, +} from './telemetry_collection'; + +export type { + TelemetryLocalStats, DataTelemetryIndex, DataTelemetryPayload, - buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index def1131dfb1a3..c93b7e872924b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,10 +7,5 @@ */ export { DATA_TELEMETRY_ID } from './constants'; - -export { - getDataTelemetry, - buildDataTelemetryPayload, - DataTelemetryPayload, - DataTelemetryIndex, -} from './get_data_telemetry'; +export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 55f9c7f0e624c..151e89a11a192 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -export { - DATA_TELEMETRY_ID, - DataTelemetryIndex, - DataTelemetryPayload, - buildDataTelemetryPayload, -} from './get_data_telemetry'; -export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; +export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; +export { getLocalStats } from './get_local_stats'; +export type { TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 4e3f046f7611f..594b53259a65f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export { +export type { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index bdced01d9eb6f..6629e479906c9 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 77077b73cf8ad..c0cd124a132c0 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export { +export type { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1bba81769f0dd..1329979860603 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index 28b04418f512d..db6ea17556ed3 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export { TelemetryManagementSectionPluginSetup } from './plugin'; +export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 48e40814b8570..2daee868ac200 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index b9e0e0a8985b1..9b009b1d9e264 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 5f48f9fb93813..d5e0d95659e58 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -export { CollectorSet, CollectorSetPublic } from './collector_set'; -export { - Collector, +export { CollectorSet } from './collector_set'; +export type { CollectorSetPublic } from './collector_set'; +export { Collector } from './collector'; +export type { AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -16,4 +17,5 @@ export { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector, UsageCollectorOptions } from './usage_collector'; +export { UsageCollector } from './usage_collector'; +export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dfc9d19b69646..dd9e6644a827d 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,17 +9,16 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { +export { Collector } from './collector'; +export type { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, - Collector, CollectorFetchContext, } from './collector'; - -export { UsageCollectionSetup } from './plugin'; +export type { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index 1a60d84e7948c..7e3f4273bbea8 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -16,7 +16,8 @@ import { import { CollectorOptions, Collector, UsageCollector } from './collector'; import { UsageCollectionSetup, CollectorFetchContext } from './index'; -export { CollectorOptions, Collector }; +export type { CollectorOptions }; +export { Collector }; const logger = loggingSystemMock.createLogger(); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 96b2c4d37e17c..68a0853994e80 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index d924882e17fbd..aab1bdb58fe59 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export { ESLicense } from './telemetry_collection'; +export type { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 4599b068b9b38..c1a11caf44f24 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ESLicense } from './get_license'; +export type { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 476f5926f757a..1221200c7548c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/**/*", From 7984745a9d9a438513cd3f5505149d0d62c529e2 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 8 Apr 2021 13:00:11 +0200 Subject: [PATCH 104/131] Don't trigger auto-refresh until previous refresh completes (#93410) --- ...n-plugins-data-public.autorefreshdonefn.md | 11 + .../kibana-plugin-plugins-data-public.md | 3 + ...a-public.waituntilnextsessioncompletes_.md | 25 +++ ...ic.waituntilnextsessioncompletesoptions.md | 20 ++ ...nextsessioncompletesoptions.waitforidle.md | 13 ++ .../public/application/dashboard_app.tsx | 24 +- src/plugins/data/README.mdx | 159 ++++++++------ src/plugins/data/public/index.ts | 3 + src/plugins/data/public/public.api.md | 47 ++-- .../data/public/query/timefilter/index.ts | 2 +- .../timefilter/lib/auto_refresh_loop.test.ts | 205 ++++++++++++++++++ .../query/timefilter/lib/auto_refresh_loop.ts | 80 +++++++ .../query/timefilter/timefilter.test.ts | 45 +++- .../public/query/timefilter/timefilter.ts | 30 +-- .../timefilter/timefilter_service.mock.ts | 2 +- src/plugins/data/public/search/index.ts | 2 + .../data/public/search/session/index.ts | 4 + .../search/session/session_helpers.test.ts | 88 ++++++++ .../public/search/session/session_helpers.ts | 48 ++++ .../public/application/angular/discover.js | 26 ++- src/plugins/expressions/public/loader.ts | 7 +- .../public/embeddable/visualize_embeddable.ts | 8 +- .../components/visualize_top_nav.tsx | 8 +- .../lens/public/app_plugin/app.test.tsx | 6 +- x-pack/plugins/lens/public/app_plugin/app.tsx | 15 +- 25 files changed, 755 insertions(+), 126 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md create mode 100644 src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts create mode 100644 src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts create mode 100644 src/plugins/data/public/search/session/session_helpers.test.ts create mode 100644 src/plugins/data/public/search/session/session_helpers.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md new file mode 100644 index 0000000000000..a5694ea2d1af9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) + +## AutoRefreshDoneFn type + +Signature: + +```typescript +export declare type AutoRefreshDoneFn = () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index d2e7ef9db05e8..4429f45f55645 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -47,6 +47,7 @@ | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | +| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. | ## Interfaces @@ -92,6 +93,7 @@ | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | | [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | +| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | ## Variables @@ -141,6 +143,7 @@ | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. | | [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* | +| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md new file mode 100644 index 0000000000000..a4b294fb1decd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) + +## waitUntilNextSessionCompletes$() function + +Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. + +Signature: + +```typescript +export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| sessionService | ISessionService | [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| { waitForIdle } | WaitUntilNextSessionCompletesOptions | | + +Returns: + +`import("rxjs").Observable` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md new file mode 100644 index 0000000000000..d575722a22453 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) + +## WaitUntilNextSessionCompletesOptions interface + +Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) + +Signature: + +```typescript +export interface WaitUntilNextSessionCompletesOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | number | For how long to wait between session state transitions before considering that session completed | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md new file mode 100644 index 0000000000000..60d3df7783852 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) + +## WaitUntilNextSessionCompletesOptions.waitForIdle property + +For how long to wait between session state transitions before considering that session completed + +Signature: + +```typescript +waitForIdle?: number; +``` diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 3d6f08f321977..e7e2ccfd46b9c 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -10,7 +10,7 @@ import { History } from 'history'; import { merge, Subject, Subscription } from 'rxjs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { debounceTime, tap } from 'rxjs/operators'; +import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; import { useKibana } from '../../../kibana_react/public'; import { DashboardConstants } from '../dashboard_constants'; import { DashboardTopNav } from './top_nav/dashboard_top_nav'; @@ -30,7 +30,7 @@ import { useSavedDashboard, } from './hooks'; -import { IndexPattern } from '../services/data'; +import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data'; import { EmbeddableRenderer } from '../services/embeddable'; import { DashboardContainerInput } from '.'; import { leaveConfirmStrings } from '../dashboard_strings'; @@ -209,14 +209,26 @@ export function DashboardApp({ ); subscriptions.add( - merge( - data.query.timefilter.timefilter.getAutoRefreshFetch$(), - searchSessionIdQuery$ - ).subscribe(() => { + searchSessionIdQuery$.subscribe(() => { triggerRefresh$.next({ force: true }); }) ); + subscriptions.add( + data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .pipe( + tap(() => { + triggerRefresh$.next({ force: true }); + }), + switchMap((done) => + // best way on a dashboard to estimate that panels are updated is to rely on search session service state + waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done)) + ) + ) + .subscribe() + ); + dashboardStateManager.registerChangeListener(() => { setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter)); // we aren't checking dirty state because there are changes the container needs to know about diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 60e74a3fa126c..30006e2b497bd 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -5,7 +5,7 @@ title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. date: 2020-12-02 -tags: ['kibana','dev', 'contributor', 'api docs'] +tags: ['kibana', 'dev', 'contributor', 'api docs'] --- # data @@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints: - Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}` - Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}` - ### Index Patterns API Index Patterns REST API allows you to create, retrieve and delete index patterns. I also @@ -212,11 +211,10 @@ The endpoint returns the created index pattern object. ```json { - "index_pattern": {} + "index_pattern": {} } ``` - #### Fetch an index pattern by ID Retrieve an index pattern by its ID. @@ -229,23 +227,22 @@ Returns an index pattern object. ```json { - "index_pattern": { - "id": "...", - "version": "...", - "title": "...", - "type": "...", - "intervalName": "...", - "timeFieldName": "...", - "sourceFilters": [], - "fields": {}, - "typeMeta": {}, - "fieldFormats": {}, - "fieldAttrs": {} - } + "index_pattern": { + "id": "...", + "version": "...", + "title": "...", + "type": "...", + "intervalName": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {} + } } ``` - #### Delete an index pattern by ID Delete and index pattern by its ID. @@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Returns an '200 OK` response with empty body on success. - #### Partially update an index pattern by ID Update part of an index pattern. Only provided fields will be updated on the index pattern, missing fields will stay as they are persisted. These fields can be update partially: - - `title` - - `timeFieldName` - - `intervalName` - - `fields` (optionally refresh fields) - - `sourceFilters` - - `fieldFormatMap` - - `type` - - `typeMeta` + +- `title` +- `timeFieldName` +- `intervalName` +- `fields` (optionally refresh fields) +- `sourceFilters` +- `fieldFormatMap` +- `type` +- `typeMeta` Update a title of an index pattern. @@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object. ```json { - "index_pattern": { - - } + "index_pattern": {} } ``` - ### Fields API Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`. - #### Update fields Update endpoint allows you to update fields presentation metadata, such as `count`, @@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object. ```json { - "index_pattern": { - - } + "index_pattern": {} } ``` - ### Scripted Fields API Scripted Fields API provides CRUD API for scripted fields of an index pattern. @@ -487,7 +477,7 @@ Returns the field object. ```json { - "field": {} + "field": {} } ``` @@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri } ``` - ## Query The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries. It contains sub-services for each of those configurations: - - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. - - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. - - `data.query.queryString` - Responsible for the query string and query language settings. - - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. - Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. +- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. +- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. +- `data.query.queryString` - Responsible for the query string and query language settings. +- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. - A simple use case is: +Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. - ```.ts - function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { - data.query.state$.subscribe(() => { +A simple use case is: - // Constuct the query portion of the search request - const query = data.query.getEsQuery(indexPattern); +```.ts +function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { + data.query.state$.subscribe(() => { + + // Constuct the query portion of the search request + const query = data.query.getEsQuery(indexPattern); + + // Construct a request + const request = { + params: { + index: indexPattern.title, + body: { + aggs: aggConfigs.toDsl(), + query, + }, + }, + }; + + // Search with the `data.query` config + const search$ = data.search.search(request); + + ... + }); +} - // Construct a request - const request = { - params: { - index: indexPattern.title, - body: { - aggs: aggConfigs.toDsl(), - query, - }, - }, - }; +``` - // Search with the `data.query` config - const search$ = data.search.search(request); +### Timefilter - ... - }); - } +`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings. + +#### Autorefresh - ``` +Timefilter provides an API for setting and getting current auto refresh state: + +```ts +const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval(); + +data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval +``` + +Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified +when it is time to refresh data because of auto refresh. +This API expects apps to confirm when they are done with reloading the data. +The confirmation mechanism is needed to prevent excessive queue of fetches. + +``` +import { refetchData } from '../my-app' + +const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$() +autoRefreshFetch$.subscribe((done) => { + try { + await refetchData(); + } finally { + // confirm that data fetching was finished + done(); + } +}) + +function unmount() { + // don't forget to unsubscribe when leaving the app + autoRefreshFetch$.unsubscribe() +} + +``` ## Search diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c47cd6cd9740d..d2683e248b7bf 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -388,6 +388,8 @@ export { PainlessError, noSearchSessionStorageCapabilityMessage, SEARCH_SESSIONS_MANAGEMENT_ID, + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, } from './search'; export type { @@ -467,6 +469,7 @@ export { TimeHistoryContract, QueryStateChange, QueryStart, + AutoRefreshDoneFn, } from './query'; export { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ec24a9296674d..db7f814a83f79 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext { // @public (undocumented) export type AutocompleteStart = ReturnType; +// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AutoRefreshDoneFn = () => void; + // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2647,6 +2652,18 @@ export const UI_SETTINGS: { readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; }; +// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable; + +// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface WaitUntilNextSessionCompletesOptions { + waitForIdle?: number; +} + // Warnings were encountered during analysis: // @@ -2694,21 +2711,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index 83e897824d86c..3dfd4e0fe514f 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -9,7 +9,7 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; -export { Timefilter, TimefilterContract } from './timefilter'; +export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts new file mode 100644 index 0000000000000..3c8b316c3b878 --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop'; + +jest.useFakeTimers(); + +test('triggers refresh with interval', () => { + const { loop$, start, stop } = createAutoRefreshLoop(); + + const fn = jest.fn((done) => done()); + loop$.subscribe(fn); + + jest.advanceTimersByTime(5000); + expect(fn).not.toBeCalled(); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(2); + + stop(); + + jest.advanceTimersByTime(5000); + expect(fn).toHaveBeenCalledTimes(2); +}); + +test('waits for done() to be called', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done!: AutoRefreshDoneFn; + const fn = jest.fn((_done) => { + done = _done; + }); + loop$.subscribe(fn); + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + expect(done).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + + done(); + + jest.advanceTimersByTime(500); + expect(fn).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn).toHaveBeenCalledTimes(2); +}); + +test('waits for done() from multiple subscribers to be called', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + let done2!: AutoRefreshDoneFn; + const fn2 = jest.fn((_done) => { + done2 = _done; + }); + loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + done2(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); + +test('unsubscribe() resets the state', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + const fn2 = jest.fn(); + const sub2 = loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + sub2.unsubscribe(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); + +test('calling done() twice is ignored', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + const fn2 = jest.fn(); + loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); +}); + +test('calling older done() is ignored', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + // @ts-ignore + if (done1) return; + done1 = _done; + }); + loop$.subscribe(fn1); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts new file mode 100644 index 0000000000000..1e213b36e1d8b --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { defer, Subject } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; +import { once } from 'lodash'; + +export type AutoRefreshDoneFn = () => void; + +/** + * Creates a loop for timepicker's auto refresh + * It has a "confirmation" mechanism: + * When auto refresh loop emits, it won't continue automatically, + * until each subscriber calls received `done` function. + * + * @internal + */ +export const createAutoRefreshLoop = () => { + let subscribersCount = 0; + const tick = new Subject(); + + let _timeoutHandle: number; + let _timeout: number = 0; + + function start() { + stop(); + if (_timeout === 0) return; + const timeoutHandle = window.setTimeout(() => { + let pendingDoneCount = subscribersCount; + const done = () => { + if (timeoutHandle !== _timeoutHandle) return; + + pendingDoneCount--; + if (pendingDoneCount === 0) { + start(); + } + }; + tick.next(done); + }, _timeout); + + _timeoutHandle = timeoutHandle; + } + + function stop() { + window.clearTimeout(_timeoutHandle); + _timeoutHandle = -1; + } + + return { + stop: () => { + _timeout = 0; + stop(); + }, + start: (timeout: number) => { + _timeout = timeout; + if (subscribersCount > 0) { + start(); + } + }, + loop$: defer(() => { + subscribersCount++; + start(); // restart the loop on a new subscriber + return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once + }).pipe( + finalize(() => { + subscribersCount--; + if (subscribersCount === 0) { + stop(); + } else { + start(); // restart the loop to potentially unblock the interval + } + }) + ), + }; +}; diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 8e1e76ed19e6d..92ee6b0c30428 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -10,7 +10,7 @@ jest.useFakeTimers(); import sinon from 'sinon'; import moment from 'moment'; -import { Timefilter } from './timefilter'; +import { AutoRefreshDoneFn, Timefilter } from './timefilter'; import { Subscription } from 'rxjs'; import { TimeRange, RefreshInterval } from '../../../common'; import { createNowProviderMock } from '../../now_provider/mocks'; @@ -121,7 +121,7 @@ describe('setRefreshInterval', () => { beforeEach(() => { update = sinon.spy(); fetch = sinon.spy(); - autoRefreshFetch = sinon.spy(); + autoRefreshFetch = sinon.spy((done) => done()); timefilter.setRefreshInterval({ pause: false, value: 0, @@ -344,3 +344,44 @@ describe('calculateBounds', () => { expect(() => timefilter.calculateBounds(timeRange)).toThrowError(); }); }); + +describe('getAutoRefreshFetch$', () => { + test('next auto refresh loop starts after "done" called', () => { + const autoRefreshFetch = jest.fn(); + let doneCb: AutoRefreshDoneFn | undefined; + timefilter.getAutoRefreshFetch$().subscribe((done) => { + autoRefreshFetch(); + doneCb = done; + }); + timefilter.setRefreshInterval({ pause: false, value: 1000 }); + + expect(autoRefreshFetch).toBeCalledTimes(0); + jest.advanceTimersByTime(5000); + expect(autoRefreshFetch).toBeCalledTimes(1); + + if (doneCb) doneCb(); + + jest.advanceTimersByTime(1005); + expect(autoRefreshFetch).toBeCalledTimes(2); + }); + + test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => { + const autoRefreshFetch = jest.fn(); + const fetch$ = timefilter.getAutoRefreshFetch$(); + const sub1 = fetch$.subscribe((done) => { + autoRefreshFetch(); + // this done will be never called, but loop will be reset by another subscription + }); + timefilter.setRefreshInterval({ pause: false, value: 1000 }); + + expect(autoRefreshFetch).toBeCalledTimes(0); + jest.advanceTimersByTime(5000); + expect(autoRefreshFetch).toBeCalledTimes(1); + + fetch$.subscribe(autoRefreshFetch); + expect(autoRefreshFetch).toBeCalledTimes(1); + sub1.unsubscribe(); + jest.advanceTimersByTime(1005); + expect(autoRefreshFetch).toBeCalledTimes(2); + }); +}); diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 436b18f70a2f8..9894010601d2b 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -22,6 +22,9 @@ import { TimeRange, } from '../../../common'; import { TimeHistoryContract } from './time_history'; +import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop'; + +export { AutoRefreshDoneFn }; // TODO: remove! @@ -32,8 +35,6 @@ export class Timefilter { private timeUpdate$ = new Subject(); // Fired when a user changes the the autorefresh settings private refreshIntervalUpdate$ = new Subject(); - // Used when an auto refresh is triggered - private autoRefreshFetch$ = new Subject(); private fetch$ = new Subject(); private _time: TimeRange; @@ -45,11 +46,12 @@ export class Timefilter { private _isTimeRangeSelectorEnabled: boolean = false; private _isAutoRefreshSelectorEnabled: boolean = false; - private _autoRefreshIntervalId: number = 0; - private readonly timeDefaults: TimeRange; private readonly refreshIntervalDefaults: RefreshInterval; + // Used when an auto refresh is triggered + private readonly autoRefreshLoop = createAutoRefreshLoop(); + constructor( config: TimefilterConfig, timeHistory: TimeHistoryContract, @@ -86,9 +88,13 @@ export class Timefilter { return this.refreshIntervalUpdate$.asObservable(); }; - public getAutoRefreshFetch$ = () => { - return this.autoRefreshFetch$.asObservable(); - }; + /** + * Get an observable that emits when it is time to refetch data due to refresh interval + * Each subscription to this observable resets internal interval + * Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop + * Apps should use this callback to start next auto refresh loop when view finished updating + */ + public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$; public getFetch$ = () => { return this.fetch$.asObservable(); @@ -166,13 +172,9 @@ export class Timefilter { } } - // Clear the previous auto refresh interval and start a new one (if not paused) - clearInterval(this._autoRefreshIntervalId); - if (!newRefreshInterval.pause) { - this._autoRefreshIntervalId = window.setInterval( - () => this.autoRefreshFetch$.next(), - newRefreshInterval.value - ); + this.autoRefreshLoop.stop(); + if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) { + this.autoRefreshLoop.start(newRefreshInterval.value); } }; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 0f2b01f618186..c22f62f45a709 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -20,7 +20,7 @@ const createSetupContractMock = () => { getEnabledUpdated$: jest.fn(), getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), - getAutoRefreshFetch$: jest.fn(() => new Observable()), + getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()), getFetch$: jest.fn(), getTime: jest.fn(), setTime: jest.fn(), diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index fded4c46992c0..92a5c36202e6f 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -45,6 +45,8 @@ export { ISessionsClient, noSearchSessionStorageCapabilityMessage, SEARCH_SESSIONS_MANAGEMENT_ID, + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, } from './session'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index 15410400a33e6..ce578378a2fe8 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; export { noSearchSessionStorageCapabilityMessage } from './i18n'; export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +export { + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, +} from './session_helpers'; diff --git a/src/plugins/data/public/search/session/session_helpers.test.ts b/src/plugins/data/public/search/session/session_helpers.test.ts new file mode 100644 index 0000000000000..5b64e7b554d18 --- /dev/null +++ b/src/plugins/data/public/search/session/session_helpers.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { waitUntilNextSessionCompletes$ } from './session_helpers'; +import { ISessionService, SessionService } from './session_service'; +import { BehaviorSubject } from 'rxjs'; +import { SearchSessionState } from './search_session_state'; +import { NowProviderInternalContract } from '../../now_provider'; +import { coreMock } from '../../../../../core/public/mocks'; +import { createNowProviderMock } from '../../now_provider/mocks'; +import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +import { getSessionsClientMock } from './mocks'; + +let sessionService: ISessionService; +let state$: BehaviorSubject; +let nowProvider: jest.Mocked; +let currentAppId$: BehaviorSubject; + +beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext(); + const startService = coreMock.createSetup().getStartServices; + nowProvider = createNowProviderMock(); + currentAppId$ = new BehaviorSubject('app'); + sessionService = new SessionService( + initializerContext, + () => + startService().then(([coreStart, ...rest]) => [ + { + ...coreStart, + application: { + ...coreStart.application, + currentAppId$, + capabilities: { + ...coreStart.application.capabilities, + management: { + kibana: { + [SEARCH_SESSIONS_MANAGEMENT_ID]: true, + }, + }, + }, + }, + }, + ...rest, + ]), + getSessionsClientMock(), + nowProvider, + { freezeState: false } // needed to use mocks inside state container + ); + state$ = new BehaviorSubject(SearchSessionState.None); + sessionService.state$.subscribe(state$); +}); + +describe('waitUntilNextSessionCompletes$', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + test('emits when next session starts', () => { + sessionService.start(); + let untrackSearch = sessionService.trackSearch({ abort: () => {} }); + untrackSearch(); + + const next = jest.fn(); + const complete = jest.fn(); + waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete }); + expect(next).not.toBeCalled(); + + sessionService.start(); + expect(next).not.toBeCalled(); + + untrackSearch = sessionService.trackSearch({ abort: () => {} }); + untrackSearch(); + + expect(next).not.toBeCalled(); + jest.advanceTimersByTime(500); + expect(next).not.toBeCalled(); + jest.advanceTimersByTime(1000); + expect(next).toBeCalledTimes(1); + expect(complete).toBeCalled(); + }); +}); diff --git a/src/plugins/data/public/search/session/session_helpers.ts b/src/plugins/data/public/search/session/session_helpers.ts new file mode 100644 index 0000000000000..1f0a2da7e93f4 --- /dev/null +++ b/src/plugins/data/public/search/session/session_helpers.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { debounceTime, first, skipUntil } from 'rxjs/operators'; +import { ISessionService } from './session_service'; +import { SearchSessionState } from './search_session_state'; + +/** + * Options for {@link waitUntilNextSessionCompletes$} + */ +export interface WaitUntilNextSessionCompletesOptions { + /** + * For how long to wait between session state transitions before considering that session completed + */ + waitForIdle?: number; +} + +/** + * Creates an observable that emits when next search session completes. + * This utility is helpful to use in the application to delay some tasks until next session completes. + * + * @param sessionService - {@link ISessionService} + * @param opts - {@link WaitUntilNextSessionCompletesOptions} + */ +export function waitUntilNextSessionCompletes$( + sessionService: ISessionService, + { waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 } +) { + return sessionService.state$.pipe( + // wait until new session starts + skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))), + // wait until new session starts loading + skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))), + // debounce to ignore quick switches from loading <-> completed. + // that could happen between sequential search requests inside a single session + debounceTime(waitForIdle), + // then wait until it finishes + first( + (state) => + state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted + ) + ); +} diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 2c80fc111c740..3be047859d3b0 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import { merge, Subject, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, tap, filter } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; @@ -393,12 +393,11 @@ function discoverController($route, $scope) { $scope.state.index = $scope.indexPattern.id; $scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern); - $scope.opts.fetch = $scope.fetch = function () { + $scope.opts.fetch = $scope.fetch = async function () { $scope.fetchCounter++; $scope.fetchError = undefined; if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { $scope.resultState = 'none'; - return; } // Abort any in-progress requests before fetching again @@ -494,11 +493,19 @@ function discoverController($route, $scope) { showUnmappedFields, }; + // handler emitted by `timefilter.getAutoRefreshFetch$()` + // to notify when data completed loading and to start a new autorefresh loop + let autoRefreshDoneCb; const fetch$ = merge( refetch$, filterManager.getFetches$(), timefilter.getFetch$(), - timefilter.getAutoRefreshFetch$(), + timefilter.getAutoRefreshFetch$().pipe( + tap((done) => { + autoRefreshDoneCb = done; + }), + filter(() => $scope.fetchStatus !== fetchStatuses.LOADING) + ), data.query.queryString.getUpdates$(), searchSessionManager.newSearchSessionIdFromURL$ ).pipe(debounceTime(100)); @@ -508,7 +515,16 @@ function discoverController($route, $scope) { $scope, fetch$, { - next: $scope.fetch, + next: async () => { + try { + await $scope.fetch(); + } finally { + // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that + // the last fetch is completed so it starts the next auto refresh loop if needed + autoRefreshDoneCb?.(); + autoRefreshDoneCb = undefined; + } + }, }, (error) => addFatalError(core.fatalErrors, error) ) diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 65925b5a2e4c2..4165b8906a20e 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -118,12 +118,15 @@ export class ExpressionLoader { return this.execution ? (this.execution.inspect() as Adapters) : undefined; } - update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void { + async update( + expression?: string | ExpressionAstExpression, + params?: IExpressionLoaderParams + ): Promise { this.setParams(params); this.loadingSubject.next(true); if (expression) { - this.loadData(expression, this.params); + await this.loadData(expression, this.params); } else if (this.data) { this.render(this.data); } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 429dabeeef042..efb166c8975bb 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -367,8 +367,8 @@ export class VisualizeEmbeddable } } - public reload = () => { - this.handleVisUpdate(); + public reload = async () => { + await this.handleVisUpdate(); }; private async updateHandler() { @@ -395,13 +395,13 @@ export class VisualizeEmbeddable }); if (this.handler && !abortController.signal.aborted) { - this.handler.update(this.expression, expressionParams); + await this.handler.update(this.expression, expressionParams); } } private handleVisUpdate = async () => { this.handleChanges(); - this.updateHandler(); + await this.updateHandler(); }; private uiStateChangeHandler = () => { 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 256e634ac6c40..f6ef1caf9c9e0 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -183,8 +183,12 @@ const TopNav = ({ useEffect(() => { const autoRefreshFetchSub = services.data.query.timefilter.timefilter .getAutoRefreshFetch$() - .subscribe(() => { - visInstance.embeddableHandler.reload(); + .subscribe(async (done) => { + try { + await visInstance.embeddableHandler.reload(); + } finally { + done(); + } }); return () => { autoRefreshFetchSub.unsubscribe(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 20bf349f6b13a..b7dbf1bbe4d87 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -155,11 +155,7 @@ function createMockTimefilter() { getBounds: jest.fn(() => timeFilter), getRefreshInterval: () => {}, getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - return next; - }, - }), + getAutoRefreshFetch$: () => new Observable(), }; } diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index dbc10c751a649..39163101fc7bd 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -14,6 +14,7 @@ import { Toast } from 'kibana/public'; import { VisualizeFieldContext } from 'src/plugins/ui_actions/public'; import { Datatable } from 'src/plugins/expressions/public'; import { EuiBreadcrumb } from '@elastic/eui'; +import { finalize, switchMap, tap } from 'rxjs/operators'; import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; import { createKbnUrlStateStorage, @@ -37,6 +38,7 @@ import { Query, SavedQuery, syncQueryStateWithUrl, + waitUntilNextSessionCompletes$, } from '../../../../../src/plugins/data/public'; import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; @@ -193,14 +195,19 @@ export function App({ const autoRefreshSubscription = data.query.timefilter.timefilter .getAutoRefreshFetch$() - .subscribe({ - next: () => { + .pipe( + tap(() => { setState((s) => ({ ...s, searchSessionId: data.search.session.start(), })); - }, - }); + }), + switchMap((done) => + // best way in lens to estimate that all panels are updated is to rely on search session service state + waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done)) + ) + ) + .subscribe(); const kbnUrlStateStorage = createKbnUrlStateStorage({ history, From d9ef5c28d5b9a39e39374d2e0b5f92cda54668eb Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 8 Apr 2021 13:10:52 +0200 Subject: [PATCH 105/131] [ML] Fix switches positioning on the Transform and DFA wizards (#96535) * [ML] fix edit runtime mapping switch positioning * [ML] fix transform wizard switches --- .../components/runtime_mappings/runtime_mappings.tsx | 2 +- .../advanced_runtime_mappings_settings.tsx | 2 +- .../components/step_define/step_define_form.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx index d21bf67a1f51c..5b8fc82ef587b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -131,7 +131,7 @@ export const RuntimeMappings: FC = ({ actions, state }) => { defaultMessage: 'Runtime mappings', })} > - + {isPopulatedObject(runtimeMappings) ? ( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx index 277226c81c925..7965db99b335b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -91,7 +91,7 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) = defaultMessage: 'Runtime mappings', })} > - + {runtimeMappings !== undefined && Object.keys(runtimeMappings).length > 0 ? ( = React.memo((props) => { } > <> - + {/* Flex Column #1: Search Bar / Advanced Search Editor */} {searchItems.savedSearch === undefined && ( From bfc940c146885910eaa58cdff8f876770a98f907 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 8 Apr 2021 13:26:43 +0200 Subject: [PATCH 106/131] [APM] Extract server type utils to package (#96349) --- package.json | 2 + packages/kbn-io-ts-utils/jest.config.js | 13 + packages/kbn-io-ts-utils/package.json | 13 + packages/kbn-io-ts-utils/src/index.ts | 11 + .../src}/json_rt/index.test.ts | 9 +- .../kbn-io-ts-utils/src}/json_rt/index.ts | 5 +- .../src/merge_rt}/index.test.ts | 25 +- .../kbn-io-ts-utils/src/merge_rt}/index.ts | 37 +- .../src}/strict_keys_rt/index.test.ts | 28 +- .../src}/strict_keys_rt/index.ts | 78 +-- packages/kbn-io-ts-utils/tsconfig.json | 19 + .../kbn-server-route-repository/README.md | 7 + .../jest.config.js | 13 + .../kbn-server-route-repository/package.json | 16 + .../src/create_server_route_factory.ts | 38 ++ .../src/create_server_route_repository.ts | 39 ++ .../src/decode_request_params.test.ts | 122 +++++ .../src/decode_request_params.ts | 43 ++ .../src/format_request.ts | 20 + .../kbn-server-route-repository/src/index.ts | 24 + .../src/parse_endpoint.ts | 22 + .../src/route_validation_object.ts | 20 + .../src/test_types.ts | 238 ++++++++ .../src/typings.ts | 192 +++++++ .../kbn-server-route-repository/tsconfig.json | 20 + .../apm/common/latency_aggregation_types.ts | 6 +- .../runtime_types/iso_to_epoch_rt/index.ts | 5 +- .../link_preview.test.tsx | 9 +- .../CustomizeUI/CustomLink/index.test.tsx | 4 +- .../service_overview.test.tsx | 25 +- ...pm_observability_overview_fetchers.test.ts | 6 +- .../apm/public/services/rest/callApmApiSpy.ts | 24 + .../public/services/rest/createCallApmApi.ts | 83 ++- x-pack/plugins/apm/server/index.ts | 6 +- .../create_es_client/call_async_with_debug.ts | 2 +- .../create_internal_es_client/index.ts | 11 +- .../server/lib/helpers/setup_request.test.ts | 127 ++--- .../apm/server/lib/helpers/setup_request.ts | 33 +- .../create_static_index_pattern.test.ts | 36 +- .../create_static_index_pattern.ts | 8 +- .../get_apm_index_pattern_title.ts | 8 +- .../get_dynamic_index_pattern.ts | 12 +- .../settings/apm_indices/get_apm_indices.ts | 9 +- x-pack/plugins/apm/server/plugin.ts | 100 ++-- .../apm/server/routes/alerts/chart_preview.ts | 40 +- .../plugins/apm/server/routes/correlations.ts | 47 +- .../server/routes/create_api/index.test.ts | 368 ------------- .../apm/server/routes/create_api/index.ts | 185 ------- .../apm/server/routes/create_apm_api.ts | 230 -------- .../server/routes/create_apm_server_route.ts | 13 + .../create_apm_server_route_repository.ts | 15 + .../plugins/apm/server/routes/create_route.ts | 29 - .../plugins/apm/server/routes/environments.ts | 16 +- x-pack/plugins/apm/server/routes/errors.ts | 35 +- .../get_global_apm_server_route_repository.ts | 82 +++ .../apm/server/routes/index_pattern.ts | 48 +- x-pack/plugins/apm/server/routes/metrics.ts | 15 +- .../server/routes/observability_overview.ts | 22 +- .../routes/register_routes/index.test.ts | 507 ++++++++++++++++++ .../server/routes/register_routes/index.ts | 143 +++++ .../plugins/apm/server/routes/rum_client.ts | 122 +++-- .../plugins/apm/server/routes/service_map.ts | 31 +- .../apm/server/routes/service_nodes.ts | 15 +- x-pack/plugins/apm/server/routes/services.ts | 214 +++++--- .../routes/settings/agent_configuration.ts | 99 ++-- .../routes/settings/anomaly_detection.ts | 35 +- .../apm/server/routes/settings/apm_indices.ts | 28 +- .../apm/server/routes/settings/custom_link.ts | 70 ++- x-pack/plugins/apm/server/routes/traces.ts | 37 +- .../plugins/apm/server/routes/transactions.ts | 106 ++-- x-pack/plugins/apm/server/routes/typings.ts | 188 ++----- x-pack/plugins/apm/server/types.ts | 164 ++++++ .../common/apm_api_supertest.ts | 19 +- .../tests/inspect/inspect.ts | 1 - .../instances_primary_statistics.ts | 7 +- yarn.lock | 8 + 76 files changed, 2822 insertions(+), 1685 deletions(-) create mode 100644 packages/kbn-io-ts-utils/jest.config.js create mode 100644 packages/kbn-io-ts-utils/package.json create mode 100644 packages/kbn-io-ts-utils/src/index.ts rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/json_rt/index.test.ts (85%) rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/json_rt/index.ts (74%) rename {x-pack/plugins/apm/common/runtime_types/merge => packages/kbn-io-ts-utils/src/merge_rt}/index.test.ts (66%) rename {x-pack/plugins/apm/common/runtime_types/merge => packages/kbn-io-ts-utils/src/merge_rt}/index.ts (62%) rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/strict_keys_rt/index.test.ts (77%) rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/strict_keys_rt/index.ts (66%) create mode 100644 packages/kbn-io-ts-utils/tsconfig.json create mode 100644 packages/kbn-server-route-repository/README.md create mode 100644 packages/kbn-server-route-repository/jest.config.js create mode 100644 packages/kbn-server-route-repository/package.json create mode 100644 packages/kbn-server-route-repository/src/create_server_route_factory.ts create mode 100644 packages/kbn-server-route-repository/src/create_server_route_repository.ts create mode 100644 packages/kbn-server-route-repository/src/decode_request_params.test.ts create mode 100644 packages/kbn-server-route-repository/src/decode_request_params.ts create mode 100644 packages/kbn-server-route-repository/src/format_request.ts create mode 100644 packages/kbn-server-route-repository/src/index.ts create mode 100644 packages/kbn-server-route-repository/src/parse_endpoint.ts create mode 100644 packages/kbn-server-route-repository/src/route_validation_object.ts create mode 100644 packages/kbn-server-route-repository/src/test_types.ts create mode 100644 packages/kbn-server-route-repository/src/typings.ts create mode 100644 packages/kbn-server-route-repository/tsconfig.json create mode 100644 x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts delete mode 100644 x-pack/plugins/apm/server/routes/create_api/index.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/create_api/index.ts delete mode 100644 x-pack/plugins/apm/server/routes/create_apm_api.ts create mode 100644 x-pack/plugins/apm/server/routes/create_apm_server_route.ts create mode 100644 x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts delete mode 100644 x-pack/plugins/apm/server/routes/create_route.ts create mode 100644 x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts create mode 100644 x-pack/plugins/apm/server/routes/register_routes/index.test.ts create mode 100644 x-pack/plugins/apm/server/routes/register_routes/index.ts create mode 100644 x-pack/plugins/apm/server/types.ts diff --git a/package.json b/package.json index a1acf73ea26f0..ffe1a10f0bfea 100644 --- a/package.json +++ b/package.json @@ -131,10 +131,12 @@ "@kbn/crypto": "link:packages/kbn-crypto", "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:packages/kbn-legacy-logging", "@kbn/logging": "link:packages/kbn-logging", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", + "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:packages/kbn-std", "@kbn/tinymath": "link:packages/kbn-tinymath", "@kbn/ui-framework": "link:packages/kbn-ui-framework", diff --git a/packages/kbn-io-ts-utils/jest.config.js b/packages/kbn-io-ts-utils/jest.config.js new file mode 100644 index 0000000000000..1a71166fae843 --- /dev/null +++ b/packages/kbn-io-ts-utils/jest.config.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-io-ts-utils'], +}; diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json new file mode 100644 index 0000000000000..4d6f02d3f85a6 --- /dev/null +++ b/packages/kbn-io-ts-utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/io-ts-utils", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts new file mode 100644 index 0000000000000..2032127b1eb91 --- /dev/null +++ b/packages/kbn-io-ts-utils/src/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { jsonRt } from './json_rt'; +export { mergeRt } from './merge_rt'; +export { strictKeysRt } from './strict_keys_rt'; diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts similarity index 85% rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts rename to packages/kbn-io-ts-utils/src/json_rt/index.test.ts index d6c286c672d90..1220639fc7bef 100644 --- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts +++ b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts @@ -1,8 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; @@ -12,9 +13,7 @@ import { Right } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; -function getValueOrThrow>( - either: TEither -): Right { +function getValueOrThrow>(either: TEither): Right { const value = pipe( either, fold(() => { diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts b/packages/kbn-io-ts-utils/src/json_rt/index.ts similarity index 74% rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.ts rename to packages/kbn-io-ts-utils/src/json_rt/index.ts index 0207145a17be7..bc596d53db54c 100644 --- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/json_rt/index.ts @@ -1,8 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts similarity index 66% rename from x-pack/plugins/apm/common/runtime_types/merge/index.test.ts rename to packages/kbn-io-ts-utils/src/merge_rt/index.test.ts index af5a0221662d5..b25d4451895f2 100644 --- a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts +++ b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts @@ -1,18 +1,19 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { isLeft } from 'fp-ts/lib/Either'; -import { merge } from './'; +import { mergeRt } from '.'; import { jsonRt } from '../json_rt'; describe('merge', () => { it('fails on one or more errors', () => { - const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]); + const type = mergeRt(t.type({ foo: t.string }), t.type({ bar: t.number })); const result = type.decode({ foo: '' }); @@ -20,10 +21,7 @@ describe('merge', () => { }); it('merges left to right', () => { - const typeBoolean = merge([ - t.type({ foo: t.string }), - t.type({ foo: jsonRt.pipe(t.boolean) }), - ]); + const typeBoolean = mergeRt(t.type({ foo: t.string }), t.type({ foo: jsonRt.pipe(t.boolean) })); const resultBoolean = typeBoolean.decode({ foo: 'true', @@ -34,10 +32,7 @@ describe('merge', () => { foo: true, }); - const typeString = merge([ - t.type({ foo: jsonRt.pipe(t.boolean) }), - t.type({ foo: t.string }), - ]); + const typeString = mergeRt(t.type({ foo: jsonRt.pipe(t.boolean) }), t.type({ foo: t.string })); const resultString = typeString.decode({ foo: 'true', @@ -50,10 +45,10 @@ describe('merge', () => { }); it('deeply merges values', () => { - const type = merge([ + const type = mergeRt( t.type({ foo: t.type({ baz: t.string }) }), - t.type({ foo: t.type({ bar: t.string }) }), - ]); + t.type({ foo: t.type({ bar: t.string }) }) + ); const result = type.decode({ foo: { diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.ts similarity index 62% rename from x-pack/plugins/apm/common/runtime_types/merge/index.ts rename to packages/kbn-io-ts-utils/src/merge_rt/index.ts index 451edf678aabe..c582767fb5101 100644 --- a/x-pack/plugins/apm/common/runtime_types/merge/index.ts +++ b/packages/kbn-io-ts-utils/src/merge_rt/index.ts @@ -1,31 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { merge as lodashMerge } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; -import { ValuesType } from 'utility-types'; -export type MergeType< - T extends t.Any[], - U extends ValuesType = ValuesType -> = t.Type & { - _tag: 'MergeType'; - types: T; -}; +type PlainObject = Record; + +type DeepMerge = U extends PlainObject + ? T extends PlainObject + ? Omit & + { + [key in keyof U]: T extends { [k in key]: any } ? DeepMerge : U[key]; + } + : U + : U; // this is similar to t.intersection, but does a deep merge // instead of a shallow merge -export function merge( - types: [A, B] -): MergeType<[A, B]>; +export type MergeType = t.Type< + DeepMerge, t.TypeOf>, + DeepMerge, t.OutputOf> +> & { + _tag: 'MergeType'; + types: [T1, T2]; +}; + +export function mergeRt(a: T1, b: T2): MergeType; -export function merge(types: t.Any[]) { +export function mergeRt(...types: t.Any[]) { const mergeType = new t.Type( 'merge', (u): u is unknown => { diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts similarity index 77% rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts index 4212e0430ff5f..ab20ca42a283e 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts @@ -1,8 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; @@ -14,10 +15,7 @@ describe('strictKeysRt', () => { it('correctly and deeply validates object keys', () => { const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [ { - type: t.intersection([ - t.type({ foo: t.string }), - t.partial({ bar: t.string }), - ]), + type: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.string })]), passes: [{ foo: '' }, { foo: '', bar: '' }], fails: [ { foo: '', unknownKey: '' }, @@ -26,15 +24,9 @@ describe('strictKeysRt', () => { }, { type: t.type({ - path: t.union([ - t.type({ serviceName: t.string }), - t.type({ transactionType: t.string }), - ]), + path: t.union([t.type({ serviceName: t.string }), t.type({ transactionType: t.string })]), }), - passes: [ - { path: { serviceName: '' } }, - { path: { transactionType: '' } }, - ], + passes: [{ path: { serviceName: '' } }, { path: { transactionType: '' } }], fails: [ { path: { serviceName: '', unknownKey: '' } }, { path: { transactionType: '', unknownKey: '' } }, @@ -62,9 +54,7 @@ describe('strictKeysRt', () => { if (!isRight(result)) { throw new Error( - `Expected ${JSON.stringify( - value - )} to be allowed, but validation failed with ${ + `Expected ${JSON.stringify(value)} to be allowed, but validation failed with ${ result.left[0].message }` ); @@ -76,9 +66,7 @@ describe('strictKeysRt', () => { if (!isLeft(result)) { throw new Error( - `Expected ${JSON.stringify( - value - )} to be disallowed, but validation succeeded` + `Expected ${JSON.stringify(value)} to be disallowed, but validation succeeded` ); } }); diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts similarity index 66% rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts index e90ccf7eb8d31..56afdf54463f7 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts @@ -1,14 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { either, isRight } from 'fp-ts/lib/Either'; import { mapValues, difference, isPlainObject, forEach } from 'lodash'; -import { MergeType, merge } from '../merge'; +import { MergeType, mergeRt } from '../merge_rt'; /* Type that tracks validated keys, and fails when the input value @@ -21,7 +22,7 @@ type ParsableType = | t.PartialType | t.ExactType | t.InterfaceType - | MergeType; + | MergeType; function getKeysInObject>( object: T, @@ -32,17 +33,16 @@ function getKeysInObject>( const ownPrefix = prefix ? `${prefix}.${key}` : key; keys.push(ownPrefix); if (isPlainObject(object[key])) { - keys.push( - ...getKeysInObject(object[key] as Record, ownPrefix) - ); + keys.push(...getKeysInObject(object[key] as Record, ownPrefix)); } }); return keys; } -function addToContextWhenValidated< - T extends t.InterfaceType | t.PartialType ->(type: T, prefix: string): T { +function addToContextWhenValidated | t.PartialType>( + type: T, + prefix: string +): T { const validate = (input: unknown, context: t.Context) => { const result = type.validate(input, context); const keysType = context[0].type as StrictKeysType; @@ -50,36 +50,19 @@ function addToContextWhenValidated< throw new Error('Expected a top-level StrictKeysType'); } if (isRight(result)) { - keysType.trackedKeys.push( - ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`) - ); + keysType.trackedKeys.push(...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)); } return result; }; if (type._tag === 'InterfaceType') { - return new t.InterfaceType( - type.name, - type.is, - validate, - type.encode, - type.props - ) as T; + return new t.InterfaceType(type.name, type.is, validate, type.encode, type.props) as T; } - return new t.PartialType( - type.name, - type.is, - validate, - type.encode, - type.props - ) as T; + return new t.PartialType(type.name, type.is, validate, type.encode, type.props) as T; } -function trackKeysOfValidatedTypes( - type: ParsableType | t.Any, - prefix: string = '' -): t.Any { +function trackKeysOfValidatedTypes(type: ParsableType | t.Any, prefix: string = ''): t.Any { if (!('_tag' in type)) { return type; } @@ -89,27 +72,24 @@ function trackKeysOfValidatedTypes( case 'IntersectionType': { const collectionType = type as t.IntersectionType; return t.intersection( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any] ); } case 'UnionType': { const collectionType = type as t.UnionType; return t.union( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any] ); } case 'MergeType': { - const collectionType = type as MergeType; - return merge( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + const collectionType = type as MergeType; + return mergeRt( + ...(collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [ + t.Any, + t.Any + ]) ); } @@ -142,9 +122,7 @@ function trackKeysOfValidatedTypes( case 'ExactType': { const exactType = type as t.ExactType; - return t.exact( - trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps - ); + return t.exact(trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps); } default: @@ -169,17 +147,11 @@ class StrictKeysType< (input, context) => { this.trackedKeys.length = 0; return either.chain(trackedType.validate(input, context), (i) => { - const originalKeys = getKeysInObject( - input as Record - ); + const originalKeys = getKeysInObject(input as Record); const excessKeys = difference(originalKeys, this.trackedKeys); if (excessKeys.length) { - return t.failure( - i, - context, - `Excess keys are not allowed: \n${excessKeys.join('\n')}` - ); + return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`); } return t.success(i); diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json new file mode 100644 index 0000000000000..6c67518e21073 --- /dev/null +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-io-ts-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md new file mode 100644 index 0000000000000..e22205540ef31 --- /dev/null +++ b/packages/kbn-server-route-repository/README.md @@ -0,0 +1,7 @@ +# @kbn/server-route-repository + +Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition. + +## Usage + +TBD diff --git a/packages/kbn-server-route-repository/jest.config.js b/packages/kbn-server-route-repository/jest.config.js new file mode 100644 index 0000000000000..7449bb7cd3860 --- /dev/null +++ b/packages/kbn-server-route-repository/jest.config.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-server-route-repository'], +}; diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json new file mode 100644 index 0000000000000..ce1ca02d0c4f6 --- /dev/null +++ b/packages/kbn-server-route-repository/package.json @@ -0,0 +1,16 @@ +{ + "name": "@kbn/server-route-repository", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@kbn/io-ts-utils": "link:../kbn-io-ts-utils" + } +} diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts new file mode 100644 index 0000000000000..edf9bd657f995 --- /dev/null +++ b/packages/kbn-server-route-repository/src/create_server_route_factory.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { + ServerRouteCreateOptions, + ServerRouteHandlerResources, + RouteParamsRT, + ServerRoute, +} from './typings'; + +export function createServerRouteFactory< + TRouteHandlerResources extends ServerRouteHandlerResources, + TRouteCreateOptions extends ServerRouteCreateOptions +>(): < + TEndpoint extends string, + TReturnType, + TRouteParamsRT extends RouteParamsRT | undefined = undefined +>( + route: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + > +) => ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions +> { + return (route) => route; +} diff --git a/packages/kbn-server-route-repository/src/create_server_route_repository.ts b/packages/kbn-server-route-repository/src/create_server_route_repository.ts new file mode 100644 index 0000000000000..5ac89ebcac77f --- /dev/null +++ b/packages/kbn-server-route-repository/src/create_server_route_repository.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { + ServerRouteHandlerResources, + ServerRouteRepository, + ServerRouteCreateOptions, +} from './typings'; + +export function createServerRouteRepository< + TRouteHandlerResources extends ServerRouteHandlerResources = never, + TRouteCreateOptions extends ServerRouteCreateOptions = never +>(): ServerRouteRepository { + let routes: Record = {}; + + return { + add(route) { + routes = { + ...routes, + [route.endpoint]: route, + }; + + return this as any; + }, + merge(repository) { + routes = { + ...routes, + ...Object.fromEntries(repository.getRoutes().map((route) => [route.endpoint, route])), + }; + + return this as any; + }, + getRoutes: () => Object.values(routes), + }; +} diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts new file mode 100644 index 0000000000000..08ef303ad0b3a --- /dev/null +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { jsonRt } from '@kbn/io-ts-utils'; +import * as t from 'io-ts'; +import { decodeRequestParams } from './decode_request_params'; + +describe('decodeRequestParams', () => { + it('decodes request params', () => { + const decode = () => { + return decodeRequestParams( + { + params: { + serviceName: 'opbeans-java', + }, + body: null, + query: { + start: '', + }, + }, + t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + start: t.string, + }), + }) + ); + }; + expect(decode).not.toThrow(); + + expect(decode()).toEqual({ + path: { + serviceName: 'opbeans-java', + }, + query: { + start: '', + }, + }); + }); + + it('fails on excess keys', () => { + const decode = () => { + return decodeRequestParams( + { + params: { + serviceName: 'opbeans-java', + extraKey: '', + }, + body: null, + query: { + start: '', + }, + }, + t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + start: t.string, + }), + }) + ); + }; + + expect(decode).toThrowErrorMatchingInlineSnapshot(` + "Excess keys are not allowed: + path.extraKey" + `); + }); + + it('returns the decoded output', () => { + const decode = () => { + return decodeRequestParams( + { + params: {}, + query: { + _inspect: 'true', + }, + body: null, + }, + t.type({ + query: t.type({ + _inspect: jsonRt.pipe(t.boolean), + }), + }) + ); + }; + + expect(decode).not.toThrow(); + + expect(decode()).toEqual({ + query: { + _inspect: true, + }, + }); + }); + + it('strips empty params', () => { + const decode = () => { + return decodeRequestParams( + { + params: {}, + query: {}, + body: {}, + }, + t.type({ + body: t.any, + }) + ); + }; + + expect(decode).not.toThrow(); + + expect(decode()).toEqual({}); + }); +}); diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts new file mode 100644 index 0000000000000..00492d69b8ac5 --- /dev/null +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as t from 'io-ts'; +import { omitBy, isPlainObject, isEmpty } from 'lodash'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import Boom from '@hapi/boom'; +import { strictKeysRt } from '@kbn/io-ts-utils'; +import { RouteParamsRT } from './typings'; + +interface KibanaRequestParams { + body: unknown; + query: unknown; + params: unknown; +} + +export function decodeRequestParams( + params: KibanaRequestParams, + paramsRt: T +): t.OutputOf { + const paramMap = omitBy( + { + path: params.params, + body: params.body, + query: params.query, + }, + (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val)) + ); + + // decode = validate + const result = strictKeysRt(paramsRt).decode(paramMap); + + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); + } + + return result.right; +} diff --git a/packages/kbn-server-route-repository/src/format_request.ts b/packages/kbn-server-route-repository/src/format_request.ts new file mode 100644 index 0000000000000..49004a78ce0e0 --- /dev/null +++ b/packages/kbn-server-route-repository/src/format_request.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseEndpoint } from './parse_endpoint'; + +export function formatRequest(endpoint: string, pathParams: Record = {}) { + const { method, pathname: rawPathname } = parseEndpoint(endpoint); + + // replace template variables with path params + const pathname = Object.keys(pathParams).reduce((acc, paramName) => { + return acc.replace(`{${paramName}}`, pathParams[paramName]); + }, rawPathname); + + return { method, pathname }; +} diff --git a/packages/kbn-server-route-repository/src/index.ts b/packages/kbn-server-route-repository/src/index.ts new file mode 100644 index 0000000000000..23621c5b213bc --- /dev/null +++ b/packages/kbn-server-route-repository/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createServerRouteRepository } from './create_server_route_repository'; +export { createServerRouteFactory } from './create_server_route_factory'; +export { formatRequest } from './format_request'; +export { parseEndpoint } from './parse_endpoint'; +export { decodeRequestParams } from './decode_request_params'; +export { routeValidationObject } from './route_validation_object'; +export { + RouteRepositoryClient, + ReturnOf, + EndpointOf, + ClientRequestParamsOf, + DecodedRequestParamsOf, + ServerRouteRepository, + ServerRoute, + RouteParamsRT, +} from './typings'; diff --git a/packages/kbn-server-route-repository/src/parse_endpoint.ts b/packages/kbn-server-route-repository/src/parse_endpoint.ts new file mode 100644 index 0000000000000..fd40489b0f4a5 --- /dev/null +++ b/packages/kbn-server-route-repository/src/parse_endpoint.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +type Method = 'get' | 'post' | 'put' | 'delete'; + +export function parseEndpoint(endpoint: string) { + const parts = endpoint.split(' '); + + const method = parts[0].trim().toLowerCase() as Method; + const pathname = parts[1].trim(); + + if (!['get', 'post', 'put', 'delete'].includes(method)) { + throw new Error('Endpoint was not prefixed with a valid HTTP method'); + } + + return { method, pathname }; +} diff --git a/packages/kbn-server-route-repository/src/route_validation_object.ts b/packages/kbn-server-route-repository/src/route_validation_object.ts new file mode 100644 index 0000000000000..550be8d20d446 --- /dev/null +++ b/packages/kbn-server-route-repository/src/route_validation_object.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { schema } from '@kbn/config-schema'; + +const anyObject = schema.object({}, { unknowns: 'allow' }); + +export const routeValidationObject = { + // `body` can be null, but `validate` expects non-nullable types + // if any validation is defined. Not having validation currently + // means we don't get the payload. See + // https://github.com/elastic/kibana/issues/50179 + body: schema.nullable(anyObject), + params: anyObject, + query: anyObject, +}; diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts new file mode 100644 index 0000000000000..c9015e19b82f8 --- /dev/null +++ b/packages/kbn-server-route-repository/src/test_types.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as t from 'io-ts'; +import { createServerRouteRepository } from './create_server_route_repository'; +import { decodeRequestParams } from './decode_request_params'; +import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings'; + +function assertType(value: TShape) { + return value; +} + +// Generic arguments for createServerRouteRepository should be set, +// if not, registering routes should not be allowed +createServerRouteRepository().add({ + // @ts-expect-error + endpoint: 'any_endpoint', + // @ts-expect-error + handler: async ({ params }) => {}, +}); + +// If a params codec is not set, its type should not be available in +// the request handler. +createServerRouteRepository<{}, {}>().add({ + endpoint: 'endpoint_without_params', + handler: async (resources) => { + // @ts-expect-error Argument of type '{}' is not assignable to parameter of type '{ params: any; }'. + assertType<{ params: any }>(resources); + }, +}); + +// If a params codec is set, its type _should_ be available in the +// request handler. +createServerRouteRepository<{}, {}>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async (resources) => { + assertType<{ params: { path: { serviceName: string } } }>(resources); + }, +}); + +// Resources should be passed to the request handler. +createServerRouteRepository<{ context: { getSpaceId: () => string } }, {}>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async ({ context }) => { + const spaceId = context.getSpaceId(); + assertType(spaceId); + }, +}); + +// Create options are available when registering a route. +createServerRouteRepository<{}, { options: { tags: string[] } }>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + options: { + tags: [], + }, + handler: async (resources) => { + assertType<{ params: { path: { serviceName: string } } }>(resources); + }, +}); + +const repository = createServerRouteRepository<{}, {}>() + .add({ + endpoint: 'endpoint_without_params', + handler: async () => { + return { + noParamsForMe: true, + }; + }, + }) + .add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async () => { + return { + yesParamsForMe: true, + }; + }, + }) + .add({ + endpoint: 'endpoint_with_optional_params', + params: t.partial({ + query: t.partial({ + serviceName: t.string, + }), + }), + handler: async () => { + return { + someParamsForMe: true, + }; + }, + }); + +type TestRepository = typeof repository; + +// EndpointOf should return all valid endpoints of a repository + +assertType>>([ + 'endpoint_with_params', + 'endpoint_without_params', + 'endpoint_with_optional_params', +]); + +// @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"' +assertType>>(['this_endpoint_does_not_exist']); + +// ReturnOf should return the return type of a request handler. + +assertType>({ + noParamsForMe: true, +}); + +const noParamsInvalid: ReturnOf = { + // @ts-expect-error type '{ paramsForMe: boolean; }' is not assignable to type '{ noParamsForMe: boolean; }'. + paramsForMe: true, +}; + +// RouteRepositoryClient + +type TestClient = RouteRepositoryClient; + +const client: TestClient = {} as any; + +// It should respect any additional create options. + +// @ts-expect-error Property 'timeout' is missing +client({ + endpoint: 'endpoint_without_params', +}); + +client({ + endpoint: 'endpoint_without_params', + timeout: 1, +}); + +// It does not allow params for routes without a params codec +client({ + endpoint: 'endpoint_without_params', + // @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type + params: {}, + timeout: 1, +}); + +// It requires params for routes with a params codec +client({ + endpoint: 'endpoint_with_params', + params: { + // @ts-expect-error property 'serviceName' is missing in type '{}' + path: {}, + }, + timeout: 1, +}); + +// Params are optional if the codec has no required keys +client({ + endpoint: 'endpoint_with_optional_params', + timeout: 1, +}); + +// If optional, an error will still occur if the params do not match +client({ + endpoint: 'endpoint_with_optional_params', + timeout: 1, + params: { + // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type + path: '', + }, +}); + +// The return type is correctly inferred +client({ + endpoint: 'endpoint_with_params', + params: { + path: { + serviceName: '', + }, + }, + timeout: 1, +}).then((res) => { + assertType<{ + noParamsForMe: boolean; + // @ts-expect-error Property 'noParamsForMe' is missing in type + }>(res); + + assertType<{ + yesParamsForMe: boolean; + }>(res); +}); + +// decodeRequestParams should return the type of the codec that is passed +assertType<{ path: { serviceName: string } }>( + decodeRequestParams( + { + params: { + serviceName: 'serviceName', + }, + body: undefined, + query: undefined, + }, + t.type({ path: t.type({ serviceName: t.string }) }) + ) +); + +assertType<{ path: { serviceName: boolean } }>( + // @ts-expect-error The types of 'path.serviceName' are incompatible between these types. + decodeRequestParams( + { + params: { + serviceName: 'serviceName', + }, + body: undefined, + query: undefined, + }, + t.type({ path: t.type({ serviceName: t.string }) }) + ) +); diff --git a/packages/kbn-server-route-repository/src/typings.ts b/packages/kbn-server-route-repository/src/typings.ts new file mode 100644 index 0000000000000..c27f67c71e88b --- /dev/null +++ b/packages/kbn-server-route-repository/src/typings.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { RequiredKeys } from 'utility-types'; + +type MaybeOptional }> = RequiredKeys< + T['params'] +> extends never + ? { params?: T['params'] } + : { params: T['params'] }; + +type WithoutIncompatibleMethods = Omit & { + encode: t.Encode; + asEncoder: () => t.Encoder; +}; + +export type RouteParamsRT = WithoutIncompatibleMethods< + t.Type<{ + path?: any; + query?: any; + body?: any; + }> +>; + +export interface RouteState { + [endpoint: string]: ServerRoute; +} + +export type ServerRouteHandlerResources = Record; +export type ServerRouteCreateOptions = Record; + +export type ServerRoute< + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined, + TRouteHandlerResources extends ServerRouteHandlerResources, + TReturnType, + TRouteCreateOptions extends ServerRouteCreateOptions +> = { + endpoint: TEndpoint; + params?: TRouteParamsRT; + handler: ({}: TRouteHandlerResources & + (TRouteParamsRT extends RouteParamsRT + ? DecodedRequestParamsOfType + : {})) => Promise; +} & TRouteCreateOptions; + +export interface ServerRouteRepository< + TRouteHandlerResources extends ServerRouteHandlerResources = ServerRouteHandlerResources, + TRouteCreateOptions extends ServerRouteCreateOptions = ServerRouteCreateOptions, + TRouteState extends RouteState = RouteState +> { + add< + TEndpoint extends string, + TReturnType, + TRouteParamsRT extends RouteParamsRT | undefined = undefined + >( + route: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + > + ): ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + TRouteState & + { + [key in TEndpoint]: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + >; + } + >; + merge< + TServerRouteRepository extends ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions + > + >( + repository: TServerRouteRepository + ): TServerRouteRepository extends ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + infer TRouteStateToMerge + > + ? ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + TRouteState & TRouteStateToMerge + > + : never; + getRoutes: () => Array< + ServerRoute + >; +} + +type ClientRequestParamsOfType< + TRouteParamsRT extends RouteParamsRT +> = TRouteParamsRT extends t.Mixed + ? MaybeOptional<{ + params: t.OutputOf; + }> + : {}; + +type DecodedRequestParamsOfType< + TRouteParamsRT extends RouteParamsRT +> = TRouteParamsRT extends t.Mixed + ? MaybeOptional<{ + params: t.TypeOf; + }> + : {}; + +export type EndpointOf< + TServerRouteRepository extends ServerRouteRepository +> = TServerRouteRepository extends ServerRouteRepository + ? keyof TRouteState + : never; + +export type ReturnOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + any, + any, + infer TReturnType, + ServerRouteCreateOptions + > + ? TReturnType + : never + : never + : never; + +export type DecodedRequestParamsOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + infer TRouteParamsRT, + any, + any, + ServerRouteCreateOptions + > + ? TRouteParamsRT extends RouteParamsRT + ? DecodedRequestParamsOfType + : {} + : never + : never + : never; + +export type ClientRequestParamsOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + infer TRouteParamsRT, + any, + any, + ServerRouteCreateOptions + > + ? TRouteParamsRT extends RouteParamsRT + ? ClientRequestParamsOfType + : {} + : never + : never + : never; + +export type RouteRepositoryClient< + TServerRouteRepository extends ServerRouteRepository, + TAdditionalClientOptions extends Record +> = >( + options: { + endpoint: TEndpoint; + } & ClientRequestParamsOf & + TAdditionalClientOptions +) => Promise>; diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json new file mode 100644 index 0000000000000..8f1e72172c675 --- /dev/null +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-server-route-repository/src", + "types": [ + "jest", + "node" + ], + "noUnusedLocals": false + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/x-pack/plugins/apm/common/latency_aggregation_types.ts b/x-pack/plugins/apm/common/latency_aggregation_types.ts index d9db58f223144..964d6f4ed1015 100644 --- a/x-pack/plugins/apm/common/latency_aggregation_types.ts +++ b/x-pack/plugins/apm/common/latency_aggregation_types.ts @@ -14,7 +14,7 @@ export enum LatencyAggregationType { } export const latencyAggregationTypeRt = t.union([ - t.literal('avg'), - t.literal('p95'), - t.literal('p99'), + t.literal(LatencyAggregationType.avg), + t.literal(LatencyAggregationType.p95), + t.literal(LatencyAggregationType.p99), ]); diff --git a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts index 1a17f82a52141..970e39bc4f86f 100644 --- a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts +++ b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts @@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type( ? t.failure(input, context) : t.success(epochDate); }), - (a) => { - const d = new Date(a); - return d.toISOString(); - } + (output) => new Date(output).toISOString() ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 6a6db40892e10..407f460f25ad3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -14,15 +14,18 @@ import { act, waitFor, } from '@testing-library/react'; -import * as apmApi from '../../../../../../services/rest/createCallApmApi'; +import { + getCallApmApiSpy, + CallApmApiSpy, +} from '../../../../../../services/rest/callApmApiSpy'; export const removeExternalLinkText = (str: string) => str.replace(/\(opens in a new tab or window\)/g, ''); describe('LinkPreview', () => { - let callApmApiSpy: jest.SpyInstance; + let callApmApiSpy: CallApmApiSpy; beforeAll(() => { - callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({ + callApmApiSpy = getCallApmApiSpy().mockResolvedValue({ transaction: { id: 'foo' }, }); }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 77835afef863a..7d119b8c406da 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -8,6 +8,7 @@ import { fireEvent, render, RenderResult } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; +import { getCallApmApiSpy } from '../../../../../services/rest/callApmApiSpy'; import { CustomLinkOverview } from '.'; import { License } from '../../../../../../../licensing/common/license'; import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context'; @@ -17,7 +18,6 @@ import { } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../../../context/license/license_context'; import * as hooks from '../../../../../hooks/use_fetcher'; -import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -43,7 +43,7 @@ function getMockAPMContext({ canSave }: { canSave: boolean }) { describe('CustomLink', () => { beforeAll(() => { - jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); + getCallApmApiSpy().mockResolvedValue({}); }); afterAll(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index b30faac7a65af..c6ed4e640693f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -22,9 +22,12 @@ import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_b import { renderWithTheme } from '../../../utils/testHelpers'; import { ServiceOverview } from './'; import { waitFor } from '@testing-library/dom'; -import * as callApmApiModule from '../../../services/rest/createCallApmApi'; import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { + getCallApmApiSpy, + getCreateCallApmApiSpy, +} from '../../../services/rest/callApmApiSpy'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -83,10 +86,10 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { - error_groups: [], + error_groups: [] as any[], }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { - transactionGroups: [], + transactionGroups: [] as any[], totalTransactionGroups: 0, isAggregationAccurate: true, }, @@ -95,19 +98,17 @@ describe('ServiceOverview', () => { }; /* eslint-enable @typescript-eslint/naming-convention */ - jest - .spyOn(callApmApiModule, 'createCallApmApi') - .mockImplementation(() => {}); - - const callApmApi = jest - .spyOn(callApmApiModule, 'callApmApi') - .mockImplementation(({ endpoint }) => { + const callApmApiSpy = getCallApmApiSpy().mockImplementation( + ({ endpoint }) => { const response = calls[endpoint as keyof typeof calls]; return response ? Promise.resolve(response) : Promise.reject(`Response for ${endpoint} is not defined`); - }); + } + ); + + getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); jest .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') .mockReturnValue({ @@ -124,7 +125,7 @@ describe('ServiceOverview', () => { ); await waitFor(() => - expect(callApmApi).toHaveBeenCalledTimes(Object.keys(calls).length) + expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length) ); expect((await findAllByText('Latency')).length).toBeGreaterThan(0); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 29fabc51fd582..00447607cf787 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -10,10 +10,10 @@ import { fetchObservabilityOverviewPageData, getHasData, } from './apm_observability_overview_fetchers'; -import * as createCallApmApi from './createCallApmApi'; +import { getCallApmApiSpy } from './callApmApiSpy'; describe('Observability dashboard data', () => { - const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); + const callApmApiMock = getCallApmApiSpy(); const params = { absoluteTime: { start: moment('2020-07-02T13:25:11.629Z').valueOf(), @@ -84,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionPerMinute: { value: null, timeseries: [] }, + transactionPerMinute: { value: null, timeseries: [] as any }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts new file mode 100644 index 0000000000000..ba9f740e06d0d --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as createCallApmApi from './createCallApmApi'; +import type { AbstractAPMClient } from './createCallApmApi'; + +export type CallApmApiSpy = jest.SpyInstance< + Promise, + Parameters +>; + +export type CreateCallApmApiSpy = jest.SpyInstance; + +export const getCreateCallApmApiSpy = () => + (jest.spyOn( + createCallApmApi, + 'createCallApmApi' + ) as unknown) as CreateCallApmApiSpy; +export const getCallApmApiSpy = () => + (jest.spyOn(createCallApmApi, 'callApmApi') as unknown) as CallApmApiSpy; diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index b0cce3296fe21..0e82d70faf1e1 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -6,30 +6,68 @@ */ import { CoreSetup, CoreStart } from 'kibana/public'; -import { parseEndpoint } from '../../../common/apm_api/parse_endpoint'; +import * as t from 'io-ts'; +import type { + ClientRequestParamsOf, + EndpointOf, + ReturnOf, + RouteRepositoryClient, + ServerRouteRepository, + ServerRoute, +} from '@kbn/server-route-repository'; +import { formatRequest } from '@kbn/server-route-repository/target/format_request'; import { FetchOptions } from '../../../common/fetch_options'; import { callApi } from './callApi'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { APMAPI } from '../../../server/routes/create_apm_api'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { Client } from '../../../server/routes/typings'; - -export type APMClient = Client; -export type AutoAbortedAPMClient = Client; +import type { + APMServerRouteRepository, + InspectResponse, + APMRouteHandlerResources, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server'; export type APMClientOptions = Omit< FetchOptions, 'query' | 'body' | 'pathname' | 'signal' > & { - endpoint: string; signal: AbortSignal | null; - params?: { - body?: any; - query?: Record; - path?: Record; - }; }; +export type APMClient = RouteRepositoryClient< + APMServerRouteRepository, + APMClientOptions +>; + +export type AutoAbortedAPMClient = RouteRepositoryClient< + APMServerRouteRepository, + Omit +>; + +export type APIReturnType< + TEndpoint extends EndpointOf +> = ReturnOf & { + _inspect?: InspectResponse; +}; + +export type APIEndpoint = EndpointOf; + +export type APIClientRequestParamsOf< + TEndpoint extends EndpointOf +> = ClientRequestParamsOf; + +export type AbstractAPMRepository = ServerRouteRepository< + APMRouteHandlerResources, + {}, + Record< + string, + ServerRoute + > +>; + +export type AbstractAPMClient = RouteRepositoryClient< + AbstractAPMRepository, + APMClientOptions +>; + export let callApmApi: APMClient = () => { throw new Error( 'callApmApi has to be initialized before used. Call createCallApmApi first.' @@ -37,9 +75,13 @@ export let callApmApi: APMClient = () => { }; export function createCallApmApi(core: CoreStart | CoreSetup) { - callApmApi = ((options: APMClientOptions) => { - const { endpoint, params, ...opts } = options; - const { method, pathname } = parseEndpoint(endpoint, params?.path); + callApmApi = ((options) => { + const { endpoint, ...opts } = options; + const { params } = (options as unknown) as { + params?: Partial>; + }; + + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...opts, @@ -50,10 +92,3 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { }); }) as APMClient; } - -// infer return type from API -export type APIReturnType< - TPath extends keyof APMAPI['_S'] -> = APMAPI['_S'][TPath] extends { ret: any } - ? APMAPI['_S'][TPath]['ret'] - : unknown; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 00910353ac278..9ab56c1a303ea 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -120,5 +120,9 @@ export function mergeConfigs( export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); -export { APMPlugin, APMPluginSetup } from './plugin'; +export { APMPlugin } from './plugin'; +export { APMPluginSetup } from './types'; +export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; +export { InspectResponse, APMRouteHandlerResources } from './routes/typings'; + export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 1f0aa401bcab0..989297544c78f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -10,7 +10,7 @@ import { omit } from 'lodash'; import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; -import { inspectableEsQueriesMap } from '../../../routes/create_api'; +import { inspectableEsQueriesMap } from '../../../routes/register_routes'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 45e17c1678518..9d7434d127ead 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { KibanaRequest } from 'src/core/server'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { CreateIndexRequest, @@ -13,7 +12,7 @@ import { IndexRequest, } from '@elastic/elasticsearch/api/types'; import { unwrapEsResponse } from '../../../../../../observability/server'; -import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { APMRouteHandlerResources } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, @@ -31,11 +30,9 @@ export type APMInternalClient = ReturnType; export function createInternalESClient({ context, + debug, request, -}: { - context: APMRequestHandlerContext; - request: KibanaRequest; -}) { +}: Pick & { debug: boolean }) { const { asInternalUser } = context.core.elasticsearch.client; function callEs({ @@ -53,7 +50,7 @@ export function createInternalESClient({ title: getDebugTitle(request), body: getDebugBody(params, requestType), }), - debug: context.params.query._inspect, + debug, isCalledWithInternalUser: true, request, requestType, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index c0707d0286180..c0ff0cab88f47 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,8 +7,7 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { ProcessorEvent } from '../../../common/processor_event'; import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; @@ -32,7 +31,7 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({ }, })); -function getMockRequest() { +function getMockResources() { const esClientMock = { asCurrentUser: { search: jest.fn().mockResolvedValue({ body: {} }), @@ -42,7 +41,7 @@ function getMockRequest() { }, }; - const mockContext = ({ + const mockResources = ({ config: new Proxy( {}, { @@ -54,65 +53,69 @@ function getMockRequest() { _inspect: false, }, }, - core: { - elasticsearch: { - client: esClientMock, - }, - uiSettings: { - client: { - get: jest.fn().mockResolvedValue(false), + context: { + core: { + elasticsearch: { + client: esClientMock, }, - }, - savedObjects: { - client: { - get: jest.fn(), + uiSettings: { + client: { + get: jest.fn().mockResolvedValue(false), + }, + }, + savedObjects: { + client: { + get: jest.fn(), + }, }, }, }, plugins: { ml: undefined, }, - } as unknown) as APMRequestHandlerContext & { - core: { - elasticsearch: { - client: typeof esClientMock; - }; - uiSettings: { - client: { - get: jest.Mock; + request: { + url: '', + events: { + aborted$: { + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }, + }, + }, + } as unknown) as APMRouteHandlerResources & { + context: { + core: { + elasticsearch: { + client: typeof esClientMock; }; - }; - savedObjects: { - client: { - get: jest.Mock; + uiSettings: { + client: { + get: jest.Mock; + }; + }; + savedObjects: { + client: { + get: jest.Mock; + }; }; }; }; }; - const mockRequest = ({ - url: '', - events: { - aborted$: { - subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), - }, - }, - } as unknown) as KibanaRequest; - - return { mockContext, mockRequest }; + return mockResources; } describe('setupRequest', () => { describe('with default args', () => { it('calls callWithRequest', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.transaction] }, body: { foo: 'bar' }, }); + expect( - mockContext.core.elasticsearch.client.asCurrentUser.search + mockResources.context.core.elasticsearch.client.asCurrentUser.search ).toHaveBeenCalledWith({ index: ['apm-*'], body: { @@ -132,14 +135,14 @@ describe('setupRequest', () => { }); it('calls callWithInternalUser', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { internalClient } = await setupRequest(mockResources); await internalClient.search({ index: ['apm-*'], body: { foo: 'bar' }, } as any); expect( - mockContext.core.elasticsearch.client.asInternalUser.search + mockResources.context.core.elasticsearch.client.asInternalUser.search ).toHaveBeenCalledWith({ index: ['apm-*'], body: { @@ -151,8 +154,8 @@ describe('setupRequest', () => { describe('with a bool filter', () => { it('adds a range filter for `observer.version_major` to the existing filter', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.transaction], @@ -162,8 +165,8 @@ describe('setupRequest', () => { }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock - .calls[0][0]; + mockResources.context.core.elasticsearch.client.asCurrentUser.search + .mock.calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -178,8 +181,8 @@ describe('setupRequest', () => { }); it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search( { apm: { @@ -194,8 +197,8 @@ describe('setupRequest', () => { } ); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock - .calls[0][0]; + mockResources.context.core.elasticsearch.client.asCurrentUser.search + .mock.calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -216,15 +219,15 @@ describe('setupRequest', () => { describe('without a bool filter', () => { it('adds a range filter for `observer.version_major`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.error], }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.body).toEqual({ query: { @@ -241,12 +244,12 @@ describe('without a bool filter', () => { describe('with includeFrozen=false', () => { it('sets `ignore_throttled=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const mockResources = getMockResources(); // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); + mockResources.context.core.uiSettings.client.get.mockResolvedValue(false); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { @@ -255,7 +258,7 @@ describe('with includeFrozen=false', () => { }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.ignore_throttled).toBe(true); }); @@ -263,19 +266,19 @@ describe('with includeFrozen=false', () => { describe('with includeFrozen=true', () => { it('sets `ignore_throttled=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const mockResources = getMockResources(); // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); + mockResources.context.core.uiSettings.client.get.mockResolvedValue(true); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [] }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.ignore_throttled).toBe(false); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index fff661250c6df..40836cb6635e3 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -11,7 +11,7 @@ import { APMConfig } from '../..'; import { KibanaRequest } from '../../../../../../src/core/server'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { UIFilters } from '../../../typings/ui_filters'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { ApmIndicesConfig, getApmIndices, @@ -44,7 +44,7 @@ export interface SetupTimeRange { } interface SetupRequestParams { - query?: { + query: { _inspect?: boolean; /** @@ -64,13 +64,19 @@ type InferSetup = Setup & (TParams extends { query: { start: number } } ? { start: number } : {}) & (TParams extends { query: { end: number } } ? { end: number } : {}); -export async function setupRequest( - context: APMRequestHandlerContext, - request: KibanaRequest -): Promise> { +export async function setupRequest({ + context, + params, + core, + plugins, + request, + config, + logger, +}: APMRouteHandlerResources & { + params: TParams; +}): Promise> { return withApmSpan('setup_request', async () => { - const { config, logger } = context; - const { query } = context.params; + const { query } = params; const [indices, includeFrozen] = await Promise.all([ getApmIndices({ @@ -88,7 +94,7 @@ export async function setupRequest( indices, apmEventClient: createApmEventClient({ esClient: context.core.elasticsearch.client.asCurrentUser, - debug: context.params.query._inspect, + debug: query._inspect, request, indices, options: { includeFrozen }, @@ -96,11 +102,12 @@ export async function setupRequest( internalClient: createInternalESClient({ context, request, + debug: query._inspect, }), ml: - context.plugins.ml && isActivePlatinumLicense(context.licensing.license) + plugins.ml && isActivePlatinumLicense(context.licensing.license) ? getMlSetup( - context.plugins.ml, + plugins.ml.setup, context.core.savedObjects.client, request ) @@ -118,8 +125,8 @@ export async function setupRequest( } function getMlSetup( - ml: Required['ml'], - savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], + ml: Required['ml']['setup'], + savedObjectsClient: APMRouteHandlerResources['context']['core']['savedObjects']['client'], request: KibanaRequest ) { return { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index 19163da449b90..a5340c1220b44 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -8,21 +8,9 @@ import { createStaticIndexPattern } from './create_static_index_pattern'; import { Setup } from '../helpers/setup_request'; import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data'; -import { APMRequestHandlerContext } from '../../routes/typings'; import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client'; +import { APMConfig } from '../..'; -function getMockContext(config: Record) { - return ({ - config, - core: { - savedObjects: { - client: { - create: jest.fn(), - }, - }, - }, - } as unknown) as APMRequestHandlerContext; -} function getMockSavedObjectsClient() { return ({ create: jest.fn(), @@ -32,13 +20,13 @@ function getMockSavedObjectsClient() { describe('createStaticIndexPattern', () => { it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': false, - }); + const savedObjectsClient = getMockSavedObjectsClient(); await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': false, + } as APMConfig, savedObjectsClient, 'default' ); @@ -47,9 +35,6 @@ describe('createStaticIndexPattern', () => { it(`should not create index pattern if no APM data is found`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': true, - }); // does not have APM data jest @@ -60,7 +45,9 @@ describe('createStaticIndexPattern', () => { await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': true, + } as APMConfig, savedObjectsClient, 'default' ); @@ -69,9 +56,6 @@ describe('createStaticIndexPattern', () => { it(`should create index pattern`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': true, - }); // does have APM data jest @@ -82,7 +66,9 @@ describe('createStaticIndexPattern', () => { await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': true, + } as APMConfig, savedObjectsClient, 'default' ); diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index b91fb8342a212..e627e9ed1d6cf 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -12,20 +12,18 @@ import { } from '../../../../../../src/plugins/apm_oss/server'; import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data'; import { Setup } from '../helpers/setup_request'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js'; import { withApmSpan } from '../../utils/with_apm_span'; import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; export async function createStaticIndexPattern( setup: Setup, - context: APMRequestHandlerContext, + config: APMRouteHandlerResources['config'], savedObjectsClient: InternalSavedObjectsClient, spaceId: string | undefined ): Promise { return withApmSpan('create_static_index_pattern', async () => { - const { config } = context; - // don't autocreate APM index pattern if it's been disabled via the config if (!config['xpack.apm.autocreateApmIndexPattern']) { return false; @@ -39,7 +37,7 @@ export async function createStaticIndexPattern( } try { - const apmIndexPatternTitle = getApmIndexPatternTitle(context); + const apmIndexPatternTitle = getApmIndexPatternTitle(config); await withApmSpan('create_index_pattern_saved_object', () => savedObjectsClient.create( 'index-pattern', diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts index 41abe82de8ff2..faec64c798c7d 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; -export function getApmIndexPatternTitle(context: APMRequestHandlerContext) { - return context.config['apm_oss.indexPattern']; +export function getApmIndexPatternTitle( + config: APMRouteHandlerResources['config'] +) { + return config['apm_oss.indexPattern']; } diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 5d5e6eebb4c9f..8bbc22fbf289d 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -9,7 +9,7 @@ import { IndexPatternsFetcher, FieldDescriptor, } from '../../../../../../src/plugins/data/server'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { @@ -20,12 +20,12 @@ export interface IndexPatternTitleAndFields { // TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = ({ + config, context, -}: { - context: APMRequestHandlerContext; -}) => { + logger, +}: Pick) => { return withApmSpan('get_dynamic_index_pattern', async () => { - const indexPatternTitle = context.config['apm_oss.indexPattern']; + const indexPatternTitle = config['apm_oss.indexPattern']; const indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser @@ -50,7 +50,7 @@ export const getDynamicIndexPattern = ({ } catch (e) { const notExists = e.output?.statusCode === 404; if (notExists) { - context.logger.error( + logger.error( `Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist` ); return; diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index a1587611b0a2a..d8dbc242986a6 100644 --- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -14,7 +14,7 @@ import { APM_INDICES_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; import { APMConfig } from '../../..'; -import { APMRequestHandlerContext } from '../../../routes/typings'; +import { APMRouteHandlerResources } from '../../../routes/typings'; import { withApmSpan } from '../../../utils/with_apm_span'; type ISavedObjectsClient = Pick; @@ -91,9 +91,8 @@ const APM_UI_INDICES: ApmIndicesName[] = [ export async function getApmIndexSettings({ context, -}: { - context: APMRequestHandlerContext; -}) { + config, +}: Pick) { let apmIndicesSavedObject: PromiseReturnType; try { apmIndicesSavedObject = await getApmIndicesSavedObject( @@ -106,7 +105,7 @@ export async function getApmIndexSettings({ throw error; } } - const apmIndicesConfig = getApmIndicesConfig(context.config); + const apmIndicesConfig = getApmIndicesConfig(config); return APM_UI_INDICES.map((configurationName) => ({ configurationName, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index db96794627519..074df7eaafd3c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { combineLatest, Observable } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { CoreSetup, @@ -16,22 +16,10 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { SpacesPluginSetup } from '../../spaces/server'; +import { mapValues } from 'lodash'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; -import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; -import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; -import { ActionsPlugin } from '../../actions/server'; -import { AlertingPlugin } from '../../alerting/server'; -import { CloudSetup } from '../../cloud/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { MlPluginSetup } from '../../ml/server'; -import { ObservabilityPluginSetup } from '../../observability/server'; -import { SecurityPluginSetup } from '../../security/server'; -import { TaskManagerSetupContract } from '../../task_manager/server'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; @@ -40,23 +28,29 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_ import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; -import { createApmApi } from './routes/create_apm_api'; import { apmIndices, apmTelemetry } from './saved_objects'; import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; import { uiSettings } from './ui_settings'; -import type { ApmPluginRequestHandlerContext } from './routes/typings'; - -export interface APMPluginSetup { - config$: Observable; - getApmIndices: () => ReturnType; - createApmEventClient: (params: { - debug?: boolean; - request: KibanaRequest; - context: ApmPluginRequestHandlerContext; - }) => Promise>; -} - -export class APMPlugin implements Plugin { +import type { + ApmPluginRequestHandlerContext, + APMRouteHandlerResources, +} from './routes/typings'; +import { + APMPluginSetup, + APMPluginSetupDependencies, + APMPluginStartDependencies, +} from './types'; +import { registerRoutes } from './routes/register_routes'; +import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; + +export class APMPlugin + implements + Plugin< + APMPluginSetup, + void, + APMPluginSetupDependencies, + APMPluginStartDependencies + > { private currentConfig?: APMConfig; private logger?: Logger; constructor(private readonly initContext: PluginInitializerContext) { @@ -64,22 +58,8 @@ export class APMPlugin implements Plugin { } public setup( - core: CoreSetup, - plugins: { - spaces?: SpacesPluginSetup; - apmOss: APMOSSPluginSetup; - home: HomeServerPluginSetup; - licensing: LicensingPluginSetup; - cloud?: CloudSetup; - usageCollection?: UsageCollectionSetup; - taskManager?: TaskManagerSetupContract; - alerting?: AlertingPlugin['setup']; - actions?: ActionsPlugin['setup']; - observability?: ObservabilityPluginSetup; - features: FeaturesPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; - } + core: CoreSetup, + plugins: Omit ) { this.logger = this.initContext.logger.get(); const config$ = this.initContext.config.create(); @@ -101,11 +81,13 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = mergeConfigs( + const currentConfig = mergeConfigs( plugins.apmOss.config, this.initContext.config.get() ); + this.currentConfig = currentConfig; + if ( plugins.taskManager && plugins.usageCollection && @@ -122,8 +104,8 @@ export class APMPlugin implements Plugin { } const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider(); - plugins.home.tutorials.unregisterTutorial(ossTutorialProvider); - plugins.home.tutorials.registerTutorial(() => { + plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider); + plugins.home?.tutorials.registerTutorial(() => { const ossPart = ossTutorialProvider({}); if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) { ossPart.artifacts.application = { @@ -147,10 +129,26 @@ export class APMPlugin implements Plugin { registerFeaturesUsage({ licensingPlugin: plugins.licensing }); - createApmApi().init(core, { - config$: mergedConfig$, - logger: this.logger!, - plugins, + registerRoutes({ + core: { + setup: core, + start: () => core.getStartServices().then(([coreStart]) => coreStart), + }, + logger: this.logger, + config: currentConfig, + repository: getGlobalApmServerRouteRepository(), + plugins: mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[ + key as keyof APMPluginStartDependencies + ]; + }), + }; + }) as APMRouteHandlerResources['plugins'], }); const boundGetApmIndices = async () => diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts index 3bebcd49ec34a..0175860e93d35 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -10,7 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count'; import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate'; import { setupRequest } from '../../lib/helpers/setup_request'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; import { rangeRt } from '../default_api_types'; const alertParamsRt = t.intersection([ @@ -29,13 +30,14 @@ const alertParamsRt = t.intersection([ export type AlertParams = t.TypeOf; -export const transactionErrorRateChartPreview = createRoute({ +const transactionErrorRateChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { _inspect, ...alertParams } = params.query; const errorRateChartPreview = await getTransactionErrorRateChartPreview({ setup, @@ -46,13 +48,16 @@ export const transactionErrorRateChartPreview = createRoute({ }, }); -export const transactionErrorCountChartPreview = createRoute({ +const transactionErrorCountChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { _inspect, ...alertParams } = params.query; + const errorCountChartPreview = await getTransactionErrorCountChartPreview({ setup, alertParams, @@ -62,13 +67,16 @@ export const transactionErrorCountChartPreview = createRoute({ }, }); -export const transactionDurationChartPreview = createRoute({ +const transactionDurationChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; + + const { _inspect, ...alertParams } = params.query; const latencyChartPreview = await getTransactionDurationChartPreview({ alertParams, @@ -78,3 +86,9 @@ export const transactionDurationChartPreview = createRoute({ return { latencyChartPreview }; }, }); + +export const alertsChartPreviewRouteRepository = createApmServerRouteRepository() + .add(transactionErrorRateChartPreview) + .add(transactionDurationChartPreview) + .add(transactionErrorCountChartPreview) + .add(transactionDurationChartPreview); diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index c7c69e0774822..4728aa2e8d3f6 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -14,7 +14,8 @@ import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overal import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; const INVALID_LICENSE = i18n.translate( @@ -25,7 +26,7 @@ const INVALID_LICENSE = i18n.translate( } ); -export const correlationsLatencyDistributionRoute = createRoute({ +const correlationsLatencyDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/latency/overall_distribution', params: t.type({ query: t.intersection([ @@ -40,18 +41,19 @@ export const correlationsLatencyDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, serviceName, transactionType, transactionName, - } = context.params.query; + } = params.query; return getOverallLatencyDistribution({ environment, @@ -64,7 +66,7 @@ export const correlationsLatencyDistributionRoute = createRoute({ }, }); -export const correlationsForSlowTransactionsRoute = createRoute({ +const correlationsForSlowTransactionsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: t.type({ query: t.intersection([ @@ -85,11 +87,13 @@ export const correlationsForSlowTransactionsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, @@ -100,7 +104,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ fieldNames, maxLatency, distributionInterval, - } = context.params.query; + } = params.query; return getCorrelationsForSlowTransactions({ environment, @@ -117,7 +121,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ }, }); -export const correlationsErrorDistributionRoute = createRoute({ +const correlationsErrorDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', params: t.type({ query: t.intersection([ @@ -132,18 +136,20 @@ export const correlationsErrorDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, serviceName, transactionType, transactionName, - } = context.params.query; + } = params.query; return getOverallErrorTimeseries({ environment, @@ -156,7 +162,7 @@ export const correlationsErrorDistributionRoute = createRoute({ }, }); -export const correlationsForFailedTransactionsRoute = createRoute({ +const correlationsForFailedTransactionsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: t.type({ query: t.intersection([ @@ -174,11 +180,12 @@ export const correlationsForFailedTransactionsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, @@ -186,7 +193,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({ transactionType, transactionName, fieldNames, - } = context.params.query; + } = params.query; return getCorrelationsForFailedTransactions({ environment, @@ -199,3 +206,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({ }); }, }); + +export const correlationsRouteRepository = createApmServerRouteRepository() + .add(correlationsLatencyDistributionRoute) + .add(correlationsForSlowTransactionsRoute) + .add(correlationsErrorDistributionRoute) + .add(correlationsForFailedTransactionsRoute); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts deleted file mode 100644 index 9958b8dec0124..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { createApi } from './index'; -import { CoreSetup, Logger } from 'src/core/server'; -import { RouteParamsRT } from '../typings'; -import { BehaviorSubject } from 'rxjs'; -import { APMConfig } from '../..'; -import { jsonRt } from '../../../common/runtime_types/json_rt'; - -const getCoreMock = () => { - const get = jest.fn(); - const post = jest.fn(); - const put = jest.fn(); - const createRouter = jest.fn().mockReturnValue({ - get, - post, - put, - }); - - const mock = {} as CoreSetup; - - return { - mock: { - ...mock, - http: { - ...mock.http, - createRouter, - }, - }, - get, - post, - put, - createRouter, - context: { - measure: () => undefined, - config$: new BehaviorSubject({} as APMConfig), - logger: ({ - error: jest.fn(), - } as unknown) as Logger, - plugins: {}, - }, - }; -}; - -const initApi = (params?: RouteParamsRT) => { - const { mock, context, createRouter, get, post } = getCoreMock(); - const handlerMock = jest.fn(); - createApi() - .add(() => ({ - endpoint: 'GET /foo', - params, - options: { tags: ['access:apm'] }, - handler: handlerMock, - })) - .init(mock, context); - - const routeHandler = get.mock.calls[0][1]; - - const responseMock = { - ok: jest.fn(), - custom: jest.fn(), - }; - - const simulateRequest = (requestMock: any) => { - return routeHandler( - {}, - { - // stub default values - params: {}, - query: {}, - body: null, - ...requestMock, - }, - responseMock - ); - }; - - return { - simulateRequest, - handlerMock, - createRouter, - get, - post, - responseMock, - }; -}; - -describe('createApi', () => { - it('registers a route with the server', () => { - const { mock, context, createRouter, post, get, put } = getCoreMock(); - - createApi() - .add(() => ({ - endpoint: 'GET /foo', - options: { tags: ['access:apm'] }, - handler: async () => ({}), - })) - .add(() => ({ - endpoint: 'POST /bar', - params: t.type({ - body: t.string, - }), - options: { tags: ['access:apm'] }, - handler: async () => ({}), - })) - .add(() => ({ - endpoint: 'PUT /baz', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - })) - .add({ - endpoint: 'GET /qux', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - }) - .init(mock, context); - - expect(createRouter).toHaveBeenCalledTimes(1); - - expect(get).toHaveBeenCalledTimes(2); - expect(post).toHaveBeenCalledTimes(1); - expect(put).toHaveBeenCalledTimes(1); - - expect(get.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/foo', - validate: expect.anything(), - }); - - expect(get.mock.calls[1][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/qux', - validate: expect.anything(), - }); - - expect(post.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/bar', - validate: expect.anything(), - }); - - expect(put.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/baz', - validate: expect.anything(), - }); - }); - - describe('when validating', () => { - describe('_inspect', () => { - it('allows _inspect=true', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi(); - await simulateRequest({ query: { _inspect: 'true' } }); - - const params = handlerMock.mock.calls[0][0].context.params; - expect(params).toEqual({ query: { _inspect: true } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - - // responds with ok - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(responseMock.ok).toHaveBeenCalledWith({ - body: { _inspect: [] }, - }); - }); - - it('rejects _inspect=1', async () => { - const { simulateRequest, responseMock } = initApi(); - await simulateRequest({ query: { _inspect: 1 } }); - - // responds with error handler - expect(responseMock.ok).not.toHaveBeenCalled(); - expect(responseMock.custom).toHaveBeenCalledWith({ - body: { - attributes: { _inspect: [] }, - message: - 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', - }, - statusCode: 400, - }); - }); - - it('allows omitting _inspect', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi(); - await simulateRequest({ query: {} }); - - const params = handlerMock.mock.calls[0][0].context.params; - expect(params).toEqual({ query: { _inspect: false } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - - // responds with ok - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(responseMock.ok).toHaveBeenCalledWith({ body: {} }); - }); - }); - - it('throws if unknown parameters are provided', async () => { - const { simulateRequest, responseMock } = initApi(); - - await simulateRequest({ - query: { _inspect: true, extra: '' }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - body: { foo: 'bar' }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - params: { - foo: 'bar', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(3); - }); - - it('validates path parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - path: t.type({ - foo: t.string, - }), - }) - ); - - await simulateRequest({ - params: { - foo: 'bar', - }, - }); - - expect(handlerMock).toHaveBeenCalledTimes(1); - - expect(responseMock.ok).toHaveBeenCalledTimes(1); - expect(responseMock.custom).not.toHaveBeenCalled(); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - path: { - foo: 'bar', - }, - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - params: { - bar: 'foo', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - params: { - foo: 9, - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - params: { - foo: 'bar', - extra: '', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(3); - }); - - it('validates body parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - body: t.string, - }) - ); - - await simulateRequest({ - body: '', - }); - - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(responseMock.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - body: '', - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - body: null, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - }); - - it('validates query parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - query: t.type({ - bar: t.string, - filterNames: jsonRt.pipe(t.array(t.string)), - }), - }) - ); - - await simulateRequest({ - query: { - bar: '', - _inspect: 'true', - filterNames: JSON.stringify(['hostName', 'agentName']), - }, - }); - - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(responseMock.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - query: { - bar: '', - _inspect: true, - filterNames: ['hostName', 'agentName'], - }, - }); - - await simulateRequest({ - query: { - bar: '', - foo: '', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts deleted file mode 100644 index 87bc97d346984..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash'; -import Boom from '@hapi/boom'; -import { schema } from '@kbn/config-schema'; -import * as t from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaRequest, RouteRegistrar } from 'src/core/server'; -import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; -import agent from 'elastic-apm-node'; -import { parseMethod } from '../../../common/apm_api/parse_endpoint'; -import { merge } from '../../../common/runtime_types/merge'; -import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt'; -import { APMConfig } from '../..'; -import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings'; -import { jsonRt } from '../../../common/runtime_types/json_rt'; -import type { ApmPluginRequestHandlerContext } from '../typings'; - -const inspectRt = t.exact( - t.partial({ - query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), - }) -); - -type RouteOrRouteFactoryFn = Parameters['add']>[0]; - -const isNotEmpty = (val: any) => - val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val)); - -export const inspectableEsQueriesMap = new WeakMap< - KibanaRequest, - InspectResponse ->(); - -export function createApi() { - const routes: RouteOrRouteFactoryFn[] = []; - const api: ServerAPI<{}> = { - _S: {}, - add(route) { - routes.push((route as unknown) as RouteOrRouteFactoryFn); - return this as any; - }, - init(core, { config$, logger, plugins }) { - const router = core.http.createRouter(); - - let config = {} as APMConfig; - - config$.subscribe((val) => { - config = val; - }); - - routes.forEach((routeOrFactoryFn) => { - const route = - typeof routeOrFactoryFn === 'function' - ? routeOrFactoryFn(core) - : routeOrFactoryFn; - - const { params, endpoint, options, handler } = route; - - const [method, path] = endpoint.split(' '); - const typedRouterMethod = parseMethod(method); - - // For all runtime types with props, we create an exact - // version that will strip all keys that are unvalidated. - const anyObject = schema.object({}, { unknowns: 'allow' }); - - (router[typedRouterMethod] as RouteRegistrar< - typeof typedRouterMethod, - ApmPluginRequestHandlerContext - >)( - { - path, - options, - validate: { - // `body` can be null, but `validate` expects non-nullable types - // if any validation is defined. Not having validation currently - // means we don't get the payload. See - // https://github.com/elastic/kibana/issues/50179 - body: schema.nullable(anyObject), - params: anyObject, - query: anyObject, - }, - }, - async (context, request, response) => { - if (agent.isStarted()) { - agent.addLabels({ - plugin: 'apm', - }); - } - - // init debug queries - inspectableEsQueriesMap.set(request, []); - - try { - const validParams = validateParams(request, params); - const data = await handler({ - request, - context: { - ...context, - plugins, - params: validParams, - config, - logger, - }, - }); - - const body = { ...data }; - if (validParams.query._inspect) { - body._inspect = inspectableEsQueriesMap.get(request); - } - - // cleanup - inspectableEsQueriesMap.delete(request); - - return response.ok({ body }); - } catch (error) { - logger.error(error); - const opts = { - statusCode: 500, - body: { - message: error.message, - attributes: { - _inspect: inspectableEsQueriesMap.get(request), - }, - }, - }; - - if (Boom.isBoom(error)) { - opts.statusCode = error.output.statusCode; - } - - if (error instanceof RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; - } - - return response.custom(opts); - } - } - ); - }); - }, - }; - - return api; -} - -function validateParams( - request: KibanaRequest, - params: RouteParamsRT | undefined -) { - const paramsRt = params ? merge([params, inspectRt]) : inspectRt; - const paramMap = pickBy( - { - path: request.params, - body: request.body, - query: { - _inspect: 'false', - // @ts-ignore - ...request.query, - }, - }, - isNotEmpty - ); - - const result = strictKeysRt(paramsRt).decode(paramMap); - - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } - - // Only return values for parameters that have runtime types, - // but always include query as _inspect is always set even if - // it's not defined in the route. - return mergeLodash( - { query: { _inspect: false } }, - pickBy(result.right, isNotEmpty) - ); -} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts deleted file mode 100644 index 5b74aa4347f14..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - staticIndexPatternRoute, - dynamicIndexPatternRoute, - apmIndexPatternTitleRoute, -} from './index_pattern'; -import { createApi } from './create_api'; -import { environmentsRoute } from './environments'; -import { - errorDistributionRoute, - errorGroupsRoute, - errorsRoute, -} from './errors'; -import { - serviceAgentNameRoute, - serviceTransactionTypesRoute, - servicesRoute, - serviceNodeMetadataRoute, - serviceAnnotationsRoute, - serviceAnnotationsCreateRoute, - serviceErrorGroupsPrimaryStatisticsRoute, - serviceErrorGroupsComparisonStatisticsRoute, - serviceThroughputRoute, - serviceDependenciesRoute, - serviceMetadataDetailsRoute, - serviceMetadataIconsRoute, - serviceInstancesPrimaryStatisticsRoute, - serviceInstancesComparisonStatisticsRoute, - serviceProfilingStatisticsRoute, - serviceProfilingTimelineRoute, -} from './services'; -import { - agentConfigurationRoute, - getSingleAgentConfigurationRoute, - agentConfigurationSearchRoute, - deleteAgentConfigurationRoute, - listAgentConfigurationEnvironmentsRoute, - listAgentConfigurationServicesRoute, - createOrUpdateAgentConfigurationRoute, - agentConfigurationAgentNameRoute, -} from './settings/agent_configuration'; -import { - apmIndexSettingsRoute, - apmIndicesRoute, - saveApmIndicesRoute, -} from './settings/apm_indices'; -import { metricsChartsRoute } from './metrics'; -import { serviceNodesRoute } from './service_nodes'; -import { - tracesRoute, - tracesByIdRoute, - rootTransactionByTraceIdRoute, -} from './traces'; -import { - correlationsLatencyDistributionRoute, - correlationsForSlowTransactionsRoute, - correlationsErrorDistributionRoute, - correlationsForFailedTransactionsRoute, -} from './correlations'; -import { - transactionChartsBreakdownRoute, - transactionChartsDistributionRoute, - transactionChartsErrorRateRoute, - transactionGroupsRoute, - transactionGroupsPrimaryStatisticsRoute, - transactionLatencyChartsRoute, - transactionThroughputChartsRoute, - transactionGroupsComparisonStatisticsRoute, -} from './transactions'; -import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; -import { - createCustomLinkRoute, - updateCustomLinkRoute, - deleteCustomLinkRoute, - listCustomLinksRoute, - customLinkTransactionRoute, -} from './settings/custom_link'; -import { - observabilityOverviewHasDataRoute, - observabilityOverviewRoute, -} from './observability_overview'; -import { - anomalyDetectionJobsRoute, - createAnomalyDetectionJobsRoute, - anomalyDetectionEnvironmentsRoute, -} from './settings/anomaly_detection'; -import { - rumHasDataRoute, - rumClientMetricsRoute, - rumJSErrors, - rumLongTaskMetrics, - rumOverviewLocalFiltersRoute, - rumPageLoadDistBreakdownRoute, - rumPageLoadDistributionRoute, - rumPageViewsTrendRoute, - rumServicesRoute, - rumUrlSearch, - rumVisitorsBreakdownRoute, - rumWebCoreVitals, -} from './rum_client'; -import { - transactionErrorRateChartPreview, - transactionErrorCountChartPreview, - transactionDurationChartPreview, -} from './alerts/chart_preview'; - -const createApmApi = () => { - const api = createApi() - // index pattern - .add(staticIndexPatternRoute) - .add(dynamicIndexPatternRoute) - .add(apmIndexPatternTitleRoute) - - // Environments - .add(environmentsRoute) - - // Errors - .add(errorDistributionRoute) - .add(errorGroupsRoute) - .add(errorsRoute) - - // Services - .add(serviceAgentNameRoute) - .add(serviceTransactionTypesRoute) - .add(servicesRoute) - .add(serviceNodeMetadataRoute) - .add(serviceAnnotationsRoute) - .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsPrimaryStatisticsRoute) - .add(serviceThroughputRoute) - .add(serviceDependenciesRoute) - .add(serviceMetadataDetailsRoute) - .add(serviceMetadataIconsRoute) - .add(serviceInstancesPrimaryStatisticsRoute) - .add(serviceInstancesComparisonStatisticsRoute) - .add(serviceErrorGroupsComparisonStatisticsRoute) - .add(serviceProfilingTimelineRoute) - .add(serviceProfilingStatisticsRoute) - - // Agent configuration - .add(getSingleAgentConfigurationRoute) - .add(agentConfigurationAgentNameRoute) - .add(agentConfigurationRoute) - .add(agentConfigurationSearchRoute) - .add(deleteAgentConfigurationRoute) - .add(listAgentConfigurationEnvironmentsRoute) - .add(listAgentConfigurationServicesRoute) - .add(createOrUpdateAgentConfigurationRoute) - - // Correlations - .add(correlationsLatencyDistributionRoute) - .add(correlationsForSlowTransactionsRoute) - .add(correlationsErrorDistributionRoute) - .add(correlationsForFailedTransactionsRoute) - - // APM indices - .add(apmIndexSettingsRoute) - .add(apmIndicesRoute) - .add(saveApmIndicesRoute) - - // Metrics - .add(metricsChartsRoute) - .add(serviceNodesRoute) - - // Traces - .add(tracesRoute) - .add(tracesByIdRoute) - .add(rootTransactionByTraceIdRoute) - - // Transactions - .add(transactionChartsBreakdownRoute) - .add(transactionChartsDistributionRoute) - .add(transactionChartsErrorRateRoute) - .add(transactionGroupsRoute) - .add(transactionGroupsPrimaryStatisticsRoute) - .add(transactionLatencyChartsRoute) - .add(transactionThroughputChartsRoute) - .add(transactionGroupsComparisonStatisticsRoute) - - // Service map - .add(serviceMapRoute) - .add(serviceMapServiceNodeRoute) - - // Custom links - .add(createCustomLinkRoute) - .add(updateCustomLinkRoute) - .add(deleteCustomLinkRoute) - .add(listCustomLinksRoute) - .add(customLinkTransactionRoute) - - // Observability dashboard - .add(observabilityOverviewHasDataRoute) - .add(observabilityOverviewRoute) - - // Anomaly detection - .add(anomalyDetectionJobsRoute) - .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute) - - // User Experience app api routes - .add(rumOverviewLocalFiltersRoute) - .add(rumPageViewsTrendRoute) - .add(rumPageLoadDistributionRoute) - .add(rumPageLoadDistBreakdownRoute) - .add(rumClientMetricsRoute) - .add(rumServicesRoute) - .add(rumVisitorsBreakdownRoute) - .add(rumWebCoreVitals) - .add(rumJSErrors) - .add(rumUrlSearch) - .add(rumLongTaskMetrics) - .add(rumHasDataRoute) - - // Alerting - .add(transactionErrorCountChartPreview) - .add(transactionDurationChartPreview) - .add(transactionErrorRateChartPreview); - - return api; -}; - -export type APMAPI = ReturnType; - -export { createApmApi }; diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route.ts new file mode 100644 index 0000000000000..86330a87a8c55 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/create_apm_server_route.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteFactory } from '@kbn/server-route-repository'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; + +export const createApmServerRoute = createServerRouteFactory< + APMRouteHandlerResources, + APMRouteCreateOptions +>(); diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts new file mode 100644 index 0000000000000..b7cbe890c57db --- /dev/null +++ b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteRepository } from '@kbn/server-route-repository'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; + +export function createApmServerRouteRepository() { + return createServerRouteRepository< + APMRouteHandlerResources, + APMRouteCreateOptions + >(); +} diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts deleted file mode 100644 index d74aac0992eb4..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_route.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'src/core/server'; -import { HandlerReturn, Route, RouteParamsRT } from './typings'; - -export function createRoute< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined ->( - route: Route -): Route; - -export function createRoute< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined ->( - route: (core: CoreSetup) => Route -): (core: CoreSetup) => Route; - -export function createRoute(routeOrFactoryFn: Function | object) { - return routeOrFactoryFn; -} diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index 4aa7d7e6d412f..e06fbdf7fb6d4 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -9,10 +9,11 @@ import * as t from 'io-ts'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/environments/get_environments'; -import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const environmentsRoute = createRoute({ +const environmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/environments', params: t.type({ query: t.intersection([ @@ -23,9 +24,10 @@ export const environmentsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -39,3 +41,7 @@ export const environmentsRoute = createRoute({ return { environments }; }, }); + +export const environmentsRouteRepository = createApmServerRouteRepository().add( + environmentsRoute +); diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index f69d3fc9631d1..d6bb1d4bcbaae 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -6,14 +6,15 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { getErrorDistribution } from '../lib/errors/distribution/get_distribution'; import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const errorsRoute = createRoute({ +const errorsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors', params: t.type({ path: t.type({ @@ -30,9 +31,9 @@ export const errorsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const { serviceName } = params.path; const { environment, kuery, sortField, sortDirection } = params.query; @@ -49,7 +50,7 @@ export const errorsRoute = createRoute({ }, }); -export const errorGroupsRoute = createRoute({ +const errorGroupsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}', params: t.type({ path: t.type({ @@ -59,10 +60,11 @@ export const errorGroupsRoute = createRoute({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName, groupId } = context.params.path; - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); + const { serviceName, groupId } = params.path; + const { environment, kuery } = params.query; return getErrorGroupSample({ environment, @@ -74,7 +76,7 @@ export const errorGroupsRoute = createRoute({ }, }); -export const errorDistributionRoute = createRoute({ +const errorDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', params: t.type({ path: t.type({ @@ -90,9 +92,9 @@ export const errorDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { serviceName } = params.path; const { environment, kuery, groupId } = params.query; return getErrorDistribution({ @@ -104,3 +106,8 @@ export const errorDistributionRoute = createRoute({ }); }, }); + +export const errorsRouteRepository = createApmServerRouteRepository() + .add(errorsRoute) + .add(errorGroupsRoute) + .add(errorDistributionRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts new file mode 100644 index 0000000000000..c151752b4b6e0 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + ServerRouteRepository, + ReturnOf, + EndpointOf, +} from '@kbn/server-route-repository'; +import { PickByValue } from 'utility-types'; +import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; +import { correlationsRouteRepository } from './correlations'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentsRouteRepository } from './environments'; +import { errorsRouteRepository } from './errors'; +import { indexPatternRouteRepository } from './index_pattern'; +import { metricsRouteRepository } from './metrics'; +import { observabilityOverviewRouteRepository } from './observability_overview'; +import { rumRouteRepository } from './rum_client'; +import { serviceRouteRepository } from './services'; +import { serviceMapRouteRepository } from './service_map'; +import { serviceNodeRouteRepository } from './service_nodes'; +import { agentConfigurationRouteRepository } from './settings/agent_configuration'; +import { anomalyDetectionRouteRepository } from './settings/anomaly_detection'; +import { apmIndicesRouteRepository } from './settings/apm_indices'; +import { customLinkRouteRepository } from './settings/custom_link'; +import { traceRouteRepository } from './traces'; +import { transactionRouteRepository } from './transactions'; +import { APMRouteHandlerResources } from './typings'; + +const getTypedGlobalApmServerRouteRepository = () => { + const repository = createApmServerRouteRepository() + .merge(indexPatternRouteRepository) + .merge(environmentsRouteRepository) + .merge(errorsRouteRepository) + .merge(metricsRouteRepository) + .merge(observabilityOverviewRouteRepository) + .merge(rumRouteRepository) + .merge(serviceMapRouteRepository) + .merge(serviceNodeRouteRepository) + .merge(serviceRouteRepository) + .merge(traceRouteRepository) + .merge(transactionRouteRepository) + .merge(alertsChartPreviewRouteRepository) + .merge(correlationsRouteRepository) + .merge(agentConfigurationRouteRepository) + .merge(anomalyDetectionRouteRepository) + .merge(apmIndicesRouteRepository) + .merge(customLinkRouteRepository); + + return repository; +}; + +const getGlobalApmServerRouteRepository = () => { + return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository; +}; + +export type APMServerRouteRepository = ReturnType< + typeof getTypedGlobalApmServerRouteRepository +>; + +// Ensure no APIs return arrays (or, by proxy, the any type), +// to guarantee compatibility with _inspect. + +type CompositeEndpoint = EndpointOf; + +type EndpointReturnTypes = { + [Endpoint in CompositeEndpoint]: ReturnOf; +}; + +type ArrayLikeReturnTypes = PickByValue; + +type ViolatingEndpoints = keyof ArrayLikeReturnTypes; + +function assertType() {} + +// if any endpoint has an array-like return type, the assertion below will fail +assertType(); + +export { getGlobalApmServerRouteRepository }; diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 3b800c23135ce..aa70cde4f96ae 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -6,49 +6,67 @@ */ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; -import { createRoute } from './create_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; +import { createApmServerRoute } from './create_apm_server_route'; -export const staticIndexPatternRoute = createRoute((core) => ({ +const staticIndexPatternRoute = createApmServerRoute({ endpoint: 'POST /api/apm/index_pattern/static', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { + request, + core, + plugins: { spaces }, + config, + } = resources; + const [setup, savedObjectsClient] = await Promise.all([ - setupRequest(context, request), - getInternalSavedObjectsClient(core), + setupRequest(resources), + core + .start() + .then((coreStart) => coreStart.savedObjects.createInternalRepository()), ]); - const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request); + const spaceId = spaces?.setup.spacesService.getSpaceId(request); const didCreateIndexPattern = await createStaticIndexPattern( setup, - context, + config, savedObjectsClient, spaceId ); return { created: didCreateIndexPattern }; }, -})); +}); -export const dynamicIndexPatternRoute = createRoute({ +const dynamicIndexPatternRoute = createApmServerRoute({ endpoint: 'GET /api/apm/index_pattern/dynamic', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { - const dynamicIndexPattern = await getDynamicIndexPattern({ context }); + handler: async ({ context, config, logger }) => { + const dynamicIndexPattern = await getDynamicIndexPattern({ + context, + config, + logger, + }); return { dynamicIndexPattern }; }, }); -export const apmIndexPatternTitleRoute = createRoute({ +const indexPatternTitleRoute = createApmServerRoute({ endpoint: 'GET /api/apm/index_pattern/title', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { + handler: async ({ config }) => { return { - indexPatternTitle: getApmIndexPatternTitle(context), + indexPatternTitle: getApmIndexPatternTitle(config), }; }, }); + +export const indexPatternRouteRepository = createApmServerRouteRepository() + .add(staticIndexPatternRoute) + .add(dynamicIndexPatternRoute) + .add(indexPatternTitleRoute); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index c7e82e13d07b8..9fa2346eb72fb 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -8,10 +8,11 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; -export const metricsChartsRoute = createRoute({ +const metricsChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts', params: t.type({ path: t.type({ @@ -30,9 +31,9 @@ export const metricsChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const { serviceName } = params.path; const { agentName, environment, kuery, serviceNodeName } = params.query; return await getMetricsChartDataByAgent({ @@ -45,3 +46,7 @@ export const metricsChartsRoute = createRoute({ }); }, }); + +export const metricsRouteRepository = createApmServerRouteRepository().add( + metricsChartsRoute +); diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index 1aac2c09d01c5..d459570cf7337 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -10,30 +10,32 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; -import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { withApmSpan } from '../utils/with_apm_span'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; -export const observabilityOverviewHasDataRoute = createRoute({ +const observabilityOverviewHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_data', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const res = await getHasData({ setup }); return { hasData: res }; }, }); -export const observabilityOverviewRoute = createRoute({ +const observabilityOverviewRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview', params: t.type({ query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { bucketSize } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { bucketSize } = resources.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -54,3 +56,7 @@ export const observabilityOverviewRoute = createRoute({ }); }, }); + +export const observabilityOverviewRouteRepository = createApmServerRouteRepository() + .add(observabilityOverviewRoute) + .add(observabilityOverviewHasDataRoute); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts new file mode 100644 index 0000000000000..82b73d46da5c1 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { jsonRt } from '@kbn/io-ts-utils'; +import { createServerRouteRepository } from '@kbn/server-route-repository'; +import { ServerRoute } from '@kbn/server-route-repository/target/typings'; +import * as t from 'io-ts'; +import { CoreSetup, Logger } from 'src/core/server'; +import { APMConfig } from '../..'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; +import { registerRoutes } from './index'; + +type RegisterRouteDependencies = Parameters[0]; + +const getRegisterRouteDependencies = () => { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const createRouter = jest.fn().mockReturnValue({ + get, + post, + put, + }); + + const coreSetup = ({ + http: { + createRouter, + }, + } as unknown) as CoreSetup; + + const logger = ({ + error: jest.fn(), + } as unknown) as Logger; + + return { + mocks: { + get, + post, + put, + createRouter, + coreSetup, + logger, + }, + dependencies: ({ + core: { + setup: coreSetup, + }, + logger, + config: {} as APMConfig, + plugins: {}, + } as unknown) as RegisterRouteDependencies, + }; +}; + +const getRepository = () => + createServerRouteRepository< + APMRouteHandlerResources, + APMRouteCreateOptions + >(); + +const initApi = ( + routes: Array< + ServerRoute< + any, + t.Any, + APMRouteHandlerResources, + any, + APMRouteCreateOptions + > + > +) => { + const { mocks, dependencies } = getRegisterRouteDependencies(); + + let repository = getRepository(); + + routes.forEach((route) => { + repository = repository.add(route); + }); + + registerRoutes({ + ...dependencies, + repository, + }); + + const responseMock = { + ok: jest.fn(), + custom: jest.fn(), + }; + + const simulateRequest = (request: { + method: 'get' | 'post' | 'put'; + pathname: string; + params?: Record; + body?: unknown; + query?: Record; + }) => { + const [, registeredRouteHandler] = + mocks[request.method].mock.calls.find((call) => { + return call[0].path === request.pathname; + }) ?? []; + + const result = registeredRouteHandler( + {}, + { + params: {}, + query: {}, + body: null, + ...request, + }, + responseMock + ); + + return result; + }; + + return { + simulateRequest, + mocks: { + ...mocks, + response: responseMock, + }, + }; +}; + +describe('createApi', () => { + it('registers a route with the server', () => { + const { + mocks: { createRouter, get, post, put }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'POST /bar', + params: t.type({ + body: t.string, + }), + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'PUT /baz', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + { + endpoint: 'GET /qux', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + ]); + + expect(createRouter).toHaveBeenCalledTimes(1); + + expect(get).toHaveBeenCalledTimes(2); + expect(post).toHaveBeenCalledTimes(1); + expect(put).toHaveBeenCalledTimes(1); + + expect(get.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/foo', + validate: expect.anything(), + }); + + expect(get.mock.calls[1][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/qux', + validate: expect.anything(), + }); + + expect(post.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/bar', + validate: expect.anything(), + }); + + expect(put.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/baz', + validate: expect.anything(), + }); + }); + + describe('when validating', () => { + describe('_inspect', () => { + it('allows _inspect=true', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true' }, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: true } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledWith({ + body: { _inspect: [] }, + }); + }); + + it('rejects _inspect=1', async () => { + const handlerMock = jest.fn(); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 1 }, + }); + + // responds with error handler + expect(response.ok).not.toHaveBeenCalled(); + expect(response.custom).toHaveBeenCalledWith({ + body: { + attributes: { _inspect: [] }, + message: + 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', + }, + statusCode: 400, + }); + }); + + it('allows omitting _inspect', async () => { + const handlerMock = jest.fn(); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: {}, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: false } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledWith({ body: {} }); + }); + }); + + it('throws if unknown parameters are provided', async () => { + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true', extra: '' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: { foo: 'bar' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates path parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: [] }, + params: t.type({ + path: t.type({ + foo: t.string, + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledTimes(1); + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + path: { + foo: 'bar', + }, + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + bar: 'foo', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 9, + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + extra: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates body parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + body: t.string, + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: '', + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + body: '', + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: null, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + + it('validates query parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + query: t.type({ + bar: t.string, + filterNames: jsonRt.pipe(t.array(t.string)), + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + _inspect: 'true', + filterNames: JSON.stringify(['hostName', 'agentName']), + }, + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + query: { + bar: '', + _inspect: true, + filterNames: ['hostName', 'agentName'], + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + foo: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts new file mode 100644 index 0000000000000..3a88a496b923f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import * as t from 'io-ts'; +import { KibanaRequest, RouteRegistrar } from 'src/core/server'; +import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; +import agent from 'elastic-apm-node'; +import { ServerRouteRepository } from '@kbn/server-route-repository'; +import { merge } from 'lodash'; +import { + decodeRequestParams, + parseEndpoint, + routeValidationObject, +} from '@kbn/server-route-repository'; +import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; +import { pickKeys } from '../../../common/utils/pick_keys'; +import { APMRouteHandlerResources, InspectResponse } from '../typings'; +import type { ApmPluginRequestHandlerContext } from '../typings'; + +const inspectRt = t.exact( + t.partial({ + query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), + }) +); + +export const inspectableEsQueriesMap = new WeakMap< + KibanaRequest, + InspectResponse +>(); + +export function registerRoutes({ + core, + repository, + plugins, + logger, + config, +}: { + core: APMRouteHandlerResources['core']; + plugins: APMRouteHandlerResources['plugins']; + logger: APMRouteHandlerResources['logger']; + repository: ServerRouteRepository; + config: APMRouteHandlerResources['config']; +}) { + const routes = repository.getRoutes(); + + const router = core.setup.http.createRouter(); + + routes.forEach((route) => { + const { params, endpoint, options, handler } = route; + + const { method, pathname } = parseEndpoint(endpoint); + + (router[method] as RouteRegistrar< + typeof method, + ApmPluginRequestHandlerContext + >)( + { + path: pathname, + options, + validate: routeValidationObject, + }, + async (context, request, response) => { + if (agent.isStarted()) { + agent.addLabels({ + plugin: 'apm', + }); + } + + // init debug queries + inspectableEsQueriesMap.set(request, []); + + try { + const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt; + + const validatedParams = decodeRequestParams( + pickKeys(request, 'params', 'body', 'query'), + runtimeType + ); + + const data: Record | undefined | null = (await handler({ + request, + context, + config, + logger, + core, + plugins, + params: merge( + { + query: { + _inspect: false, + }, + }, + validatedParams + ), + })) as any; + + if (Array.isArray(data)) { + throw new Error('Return type cannot be an array'); + } + + const body = validatedParams.query?._inspect + ? { + ...data, + _inspect: inspectableEsQueriesMap.get(request), + } + : { ...data }; + + // cleanup + inspectableEsQueriesMap.delete(request); + + return response.ok({ body }); + } catch (error) { + logger.error(error); + const opts = { + statusCode: 500, + body: { + message: error.message, + attributes: { + _inspect: inspectableEsQueriesMap.get(request), + }, + }, + }; + + if (Boom.isBoom(error)) { + opts.statusCode = error.output.statusCode; + } + + if (error instanceof RequestAbortedError) { + opts.statusCode = 499; + opts.body.message = 'Client closed request'; + } + + return response.custom(opts); + } + } + ); + }); +} diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 3156acb469a72..d7f91adc0d683 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { jsonRt } from '../../common/runtime_types/json_rt'; +import { jsonRt } from '@kbn/io-ts-utils'; import { LocalUIFilterName } from '../../common/ui_filter'; import { Setup, @@ -28,9 +28,10 @@ import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config'; import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; import { Projection } from '../projections/typings'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { rangeRt } from './default_api_types'; -import { APMRequestHandlerContext } from './typings'; +import { APMRouteHandlerResources } from './typings'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -45,18 +46,18 @@ const uxQueryRt = t.intersection([ t.partial({ urlQuery: t.string, percentile: t.string }), ]); -export const rumClientMetricsRoute = createRoute({ +const rumClientMetricsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum/client-metrics', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getClientMetrics({ setup, @@ -66,18 +67,18 @@ export const rumClientMetricsRoute = createRoute({ }, }); -export const rumPageLoadDistributionRoute = createRoute({ +const rumPageLoadDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-load-distribution', params: t.type({ query: t.intersection([uxQueryRt, percentileRangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { minPercentile, maxPercentile, urlQuery }, - } = context.params; + } = resources.params; const pageLoadDistribution = await getPageLoadDistribution({ setup, @@ -90,7 +91,7 @@ export const rumPageLoadDistributionRoute = createRoute({ }, }); -export const rumPageLoadDistBreakdownRoute = createRoute({ +const rumPageLoadDistBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', params: t.type({ query: t.intersection([ @@ -100,12 +101,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { minPercentile, maxPercentile, breakdown, urlQuery }, - } = context.params; + } = resources.params; const pageLoadDistBreakdown = await getPageLoadDistBreakdown({ setup, @@ -119,18 +120,18 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ }, }); -export const rumPageViewsTrendRoute = createRoute({ +const rumPageViewsTrendRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-view-trends', params: t.type({ query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { breakdowns, urlQuery }, - } = context.params; + } = resources.params; return getPageViewTrends({ setup, @@ -140,32 +141,32 @@ export const rumPageViewsTrendRoute = createRoute({ }, }); -export const rumServicesRoute = createRoute({ +const rumServicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/services', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const rumServices = await getRumServices({ setup }); return { rumServices }; }, }); -export const rumVisitorsBreakdownRoute = createRoute({ +const rumVisitorsBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/visitor-breakdown', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery }, - } = context.params; + } = resources.params; return getVisitorBreakdown({ setup, @@ -174,18 +175,18 @@ export const rumVisitorsBreakdownRoute = createRoute({ }, }); -export const rumWebCoreVitals = createRoute({ +const rumWebCoreVitals = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/web-core-vitals', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getWebCoreVitals({ setup, @@ -195,18 +196,18 @@ export const rumWebCoreVitals = createRoute({ }, }); -export const rumLongTaskMetrics = createRoute({ +const rumLongTaskMetrics = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/long-task-metrics', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getLongTaskMetrics({ setup, @@ -216,24 +217,24 @@ export const rumLongTaskMetrics = createRoute({ }, }); -export const rumUrlSearch = createRoute({ +const rumUrlSearch = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/url-search', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) }); }, }); -export const rumJSErrors = createRoute({ +const rumJSErrors = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/js-errors', params: t.type({ query: t.intersection([ @@ -244,12 +245,12 @@ export const rumJSErrors = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { pageSize, pageIndex, urlQuery }, - } = context.params; + } = resources.params; return getJSErrors({ setup, @@ -260,14 +261,14 @@ export const rumJSErrors = createRoute({ }, }); -export const rumHasDataRoute = createRoute({ +const rumHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); return await hasRumData({ setup }); }, }); @@ -309,21 +310,22 @@ function createLocalFiltersRoute< >; queryRt: TQueryRT; }) { - return createRoute({ + return createApmServerRoute({ endpoint, params: t.type({ query: t.intersection([localUiBaseQueryRt, queryRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { uiFilters } = setup; - const { query } = context.params; + + const { query } = resources.params; const { filterNames } = query; const projection = await getProjection({ query, - context, + resources, setup, }); @@ -339,7 +341,7 @@ function createLocalFiltersRoute< }); } -export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ +const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ endpoint: 'GET /api/apm/rum/local_filters', getProjection: async ({ setup }) => { return getRumPageLoadTransactionsProjection({ @@ -357,9 +359,23 @@ type GetProjection< > = ({ query, setup, - context, + resources, }: { query: t.TypeOf; setup: Setup & SetupTimeRange; - context: APMRequestHandlerContext; + resources: APMRouteHandlerResources; }) => Promise | TProjection; + +export const rumRouteRepository = createApmServerRouteRepository() + .add(rumClientMetricsRoute) + .add(rumPageLoadDistributionRoute) + .add(rumPageLoadDistBreakdownRoute) + .add(rumPageViewsTrendRoute) + .add(rumServicesRoute) + .add(rumVisitorsBreakdownRoute) + .add(rumWebCoreVitals) + .add(rumLongTaskMetrics) + .add(rumUrlSearch) + .add(rumJSErrors) + .add(rumHasDataRoute) + .add(rumOverviewLocalFiltersRoute); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 33943d6e05d01..267479de4c102 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -11,13 +11,14 @@ import { invalidLicenseMessage } from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { environmentRt, rangeRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const serviceMapRoute = createRoute({ +const serviceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/service-map', params: t.type({ query: t.intersection([ @@ -29,8 +30,9 @@ export const serviceMapRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - if (!context.config['xpack.apm.serviceMapEnabled']) { + handler: async (resources) => { + const { config, context, params, logger } = resources; + if (!config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } if (!isActivePlatinumLicense(context.licensing.license)) { @@ -42,11 +44,10 @@ export const serviceMapRoute = createRoute({ featureName: 'serviceMaps', }); - const logger = context.logger; - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { query: { serviceName, environment }, - } = context.params; + } = params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -61,7 +62,7 @@ export const serviceMapRoute = createRoute({ }, }); -export const serviceMapServiceNodeRoute = createRoute({ +const serviceMapServiceNodeRoute = createApmServerRoute({ endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: t.type({ path: t.type({ @@ -70,19 +71,21 @@ export const serviceMapServiceNodeRoute = createRoute({ query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - if (!context.config['xpack.apm.serviceMapEnabled']) { + handler: async (resources) => { + const { config, context, params } = resources; + + if (!config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { path: { serviceName }, query: { environment }, - } = context.params; + } = params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -96,3 +99,7 @@ export const serviceMapServiceNodeRoute = createRoute({ }); }, }); + +export const serviceMapRouteRepository = createApmServerRouteRepository() + .add(serviceMapRoute) + .add(serviceMapServiceNodeRoute); diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index e9060688c63a6..a2eb12662cbca 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -6,12 +6,13 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNodes } from '../lib/service_nodes'; import { rangeRt, kueryRt } from './default_api_types'; -export const serviceNodesRoute = createRoute({ +const serviceNodesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes', params: t.type({ path: t.type({ @@ -20,9 +21,9 @@ export const serviceNodesRoute = createRoute({ query: t.intersection([kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { serviceName } = params.path; const { kuery } = params.query; @@ -30,3 +31,7 @@ export const serviceNodesRoute = createRoute({ return { serviceNodes }; }, }); + +export const serviceNodeRouteRepository = createApmServerRouteRepository().add( + serviceNodesRoute +); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index b4d25ca8b2a06..800a5bdcc5d5f 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,15 +6,12 @@ */ import Boom from '@hapi/boom'; +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { uniq } from 'lodash'; -import { - LatencyAggregationType, - latencyAggregationTypeRt, -} from '../../common/latency_aggregation_types'; +import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; import { ProfilingValueType } from '../../common/profiling'; import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt'; -import { jsonRt } from '../../common/runtime_types/json_rt'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; @@ -35,7 +32,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { withApmSpan } from '../utils/with_apm_span'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, @@ -43,15 +41,16 @@ import { rangeRt, } from './default_api_types'; -export const servicesRoute = createRoute({ +const servicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services', params: t.type({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + const { environment, kuery } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -61,21 +60,22 @@ export const servicesRoute = createRoute({ kuery, setup, searchAggregatedTransactions, - logger: context.logger, + logger, }); }, }); -export const serviceMetadataDetailsRoute = createRoute({ +const serviceMetadataDetailsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metadata/details', params: t.type({ path: t.type({ serviceName: t.string }), query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -89,16 +89,17 @@ export const serviceMetadataDetailsRoute = createRoute({ }, }); -export const serviceMetadataIconsRoute = createRoute({ +const serviceMetadataIconsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons', params: t.type({ path: t.type({ serviceName: t.string }), query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -112,7 +113,7 @@ export const serviceMetadataIconsRoute = createRoute({ }, }); -export const serviceAgentNameRoute = createRoute({ +const serviceAgentNameRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/agent_name', params: t.type({ path: t.type({ @@ -121,9 +122,10 @@ export const serviceAgentNameRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -136,7 +138,7 @@ export const serviceAgentNameRoute = createRoute({ }, }); -export const serviceTransactionTypesRoute = createRoute({ +const serviceTransactionTypesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transaction_types', params: t.type({ path: t.type({ @@ -145,9 +147,11 @@ export const serviceTransactionTypesRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + return getServiceTransactionTypes({ serviceName, setup, @@ -158,7 +162,7 @@ export const serviceTransactionTypesRoute = createRoute({ }, }); -export const serviceNodeMetadataRoute = createRoute({ +const serviceNodeMetadataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', params: t.type({ @@ -169,10 +173,11 @@ export const serviceNodeMetadataRoute = createRoute({ query: t.intersection([kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName, serviceNodeName } = context.params.path; - const { kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName, serviceNodeName } = params.path; + const { kuery } = params.query; return getServiceNodeMetadata({ kuery, @@ -183,7 +188,7 @@ export const serviceNodeMetadataRoute = createRoute({ }, }); -export const serviceAnnotationsRoute = createRoute({ +const serviceAnnotationsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', params: t.type({ path: t.type({ @@ -192,12 +197,13 @@ export const serviceAnnotationsRoute = createRoute({ query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, plugins, context, request, logger } = resources; + const { serviceName } = params.path; + const { environment } = params.query; - const { observability } = context.plugins; + const { observability } = plugins; const [ annotationsClient, @@ -205,7 +211,7 @@ export const serviceAnnotationsRoute = createRoute({ ] = await Promise.all([ observability ? withApmSpan('get_scoped_annotations_client', () => - observability.getScopedAnnotationsClient(context, request) + observability.setup.getScopedAnnotationsClient(context, request) ) : undefined, getSearchAggregatedTransactions(setup), @@ -218,12 +224,12 @@ export const serviceAnnotationsRoute = createRoute({ serviceName, annotationsClient, client: context.core.elasticsearch.client.asCurrentUser, - logger: context.logger, + logger, }); }, }); -export const serviceAnnotationsCreateRoute = createRoute({ +const serviceAnnotationsCreateRoute = createApmServerRoute({ endpoint: 'POST /api/apm/services/{serviceName}/annotation', options: { tags: ['access:apm', 'access:apm_write'], @@ -250,12 +256,17 @@ export const serviceAnnotationsCreateRoute = createRoute({ }), ]), }), - handler: async ({ request, context }) => { - const { observability } = context.plugins; + handler: async (resources) => { + const { + request, + context, + plugins: { observability }, + params, + } = resources; const annotationsClient = observability ? await withApmSpan('get_scoped_annotations_client', () => - observability.getScopedAnnotationsClient(context, request) + observability.setup.getScopedAnnotationsClient(context, request) ) : undefined; @@ -263,7 +274,7 @@ export const serviceAnnotationsCreateRoute = createRoute({ throw Boom.notFound(); } - const { body, path } = context.params; + const { body, path } = params; return withApmSpan('create_annotation', () => annotationsClient.create({ @@ -283,7 +294,7 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ +const serviceErrorGroupsPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: t.type({ @@ -300,13 +311,14 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, query: { kuery, transactionType, environment }, - } = context.params; + } = params; return getServiceErrorGroupPrimaryStatistics({ kuery, serviceName, @@ -317,7 +329,7 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ }, }); -export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ +const serviceErrorGroupsComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ @@ -337,8 +349,9 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, @@ -351,7 +364,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ comparisonStart, comparisonEnd, }, - } = context.params; + } = params; return getServiceErrorGroupPeriods({ environment, @@ -367,7 +380,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ }, }); -export const serviceThroughputRoute = createRoute({ +const serviceThroughputRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: t.type({ path: t.type({ @@ -382,16 +395,17 @@ export const serviceThroughputRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, transactionType, comparisonStart, comparisonEnd, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -432,7 +446,7 @@ export const serviceThroughputRoute = createRoute({ }, }); -export const serviceInstancesPrimaryStatisticsRoute = createRoute({ +const serviceInstancesPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics', params: t.type({ @@ -450,12 +464,16 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment, kuery, transactionType } = context.params.query; - const latencyAggregationType = (context.params.query - .latencyAggregationType as unknown) as LatencyAggregationType; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { + environment, + kuery, + transactionType, + latencyAggregationType, + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -479,7 +497,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ }, }); -export const serviceInstancesComparisonStatisticsRoute = createRoute({ +const serviceInstancesComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics', params: t.type({ @@ -500,9 +518,10 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, @@ -511,9 +530,8 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ comparisonEnd, serviceNodeIds, numBuckets, - } = context.params.query; - const latencyAggregationType = (context.params.query - .latencyAggregationType as unknown) as LatencyAggregationType; + latencyAggregationType, + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -535,7 +553,7 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ }, }); -export const serviceDependenciesRoute = createRoute({ +const serviceDependenciesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/dependencies', params: t.type({ path: t.type({ @@ -552,11 +570,11 @@ export const serviceDependenciesRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - - const { serviceName } = context.params.path; - const { environment, numBuckets } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { environment, numBuckets } = params.query; const serviceDependencies = await getServiceDependencies({ serviceName, @@ -569,7 +587,7 @@ export const serviceDependenciesRoute = createRoute({ }, }); -export const serviceProfilingTimelineRoute = createRoute({ +const serviceProfilingTimelineRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline', params: t.type({ path: t.type({ @@ -580,13 +598,13 @@ export const serviceProfilingTimelineRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, query: { environment, kuery }, - } = context.params; + } = params; const profilingTimeline = await getServiceProfilingTimeline({ kuery, @@ -599,7 +617,7 @@ export const serviceProfilingTimelineRoute = createRoute({ }, }); -export const serviceProfilingStatisticsRoute = createRoute({ +const serviceProfilingStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics', params: t.type({ path: t.type({ @@ -625,13 +643,15 @@ export const serviceProfilingStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params, logger } = resources; const { path: { serviceName }, query: { environment, kuery, valueType }, - } = context.params; + } = params; return getServiceProfilingStatistics({ kuery, @@ -639,7 +659,25 @@ export const serviceProfilingStatisticsRoute = createRoute({ environment, valueType, setup, - logger: context.logger, + logger, }); }, }); + +export const serviceRouteRepository = createApmServerRouteRepository() + .add(servicesRoute) + .add(serviceMetadataDetailsRoute) + .add(serviceMetadataIconsRoute) + .add(serviceAgentNameRoute) + .add(serviceTransactionTypesRoute) + .add(serviceNodeMetadataRoute) + .add(serviceAnnotationsRoute) + .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) + .add(serviceThroughputRoute) + .add(serviceInstancesPrimaryStatisticsRoute) + .add(serviceInstancesComparisonStatisticsRoute) + .add(serviceDependenciesRoute) + .add(serviceProfilingTimelineRoute) + .add(serviceProfilingStatisticsRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 31e8d6cc1e9f0..111e0a18c8608 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -16,7 +16,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations'; import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments'; import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service'; import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent'; import { @@ -24,34 +24,37 @@ import { agentConfigurationIntakeRt, } from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; // get list of configurations -export const agentConfigurationRoute = createRoute({ +const agentConfigurationRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const configurations = await listConfigurations({ setup }); return { configurations }; }, }); // get a single configuration -export const getSingleAgentConfigurationRoute = createRoute({ +const getSingleAgentConfigurationRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/view', params: t.partial({ query: serviceRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { name, environment } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { name, environment } = params.query; const service = { name, environment }; const config = await findExactConfiguration({ service, setup }); if (!config) { - context.logger.info( + logger.info( `Config was not found for ${service.name}/${service.environment}` ); @@ -63,7 +66,7 @@ export const getSingleAgentConfigurationRoute = createRoute({ }); // delete configuration -export const deleteAgentConfigurationRoute = createRoute({ +const deleteAgentConfigurationRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], @@ -73,20 +76,22 @@ export const deleteAgentConfigurationRoute = createRoute({ service: serviceRt, }), }), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { service } = context.params.body; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { service } = params.body; const config = await findExactConfiguration({ service, setup }); if (!config) { - context.logger.info( + logger.info( `Config was not found for ${service.name}/${service.environment}` ); throw Boom.notFound(); } - context.logger.info( + logger.info( `Deleting config ${service.name}/${service.environment} (${config._id})` ); @@ -98,7 +103,7 @@ export const deleteAgentConfigurationRoute = createRoute({ }); // create/update configuration -export const createOrUpdateAgentConfigurationRoute = createRoute({ +const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ endpoint: 'PUT /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], @@ -107,9 +112,10 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({ t.partial({ query: t.partial({ overwrite: toBooleanRt }) }), t.type({ body: agentConfigurationIntakeRt }), ]), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { body, query } = context.params; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + const { body, query } = params; // if the config already exists, it is fetched and updated // this is to avoid creating two configs with identical service params @@ -125,13 +131,13 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({ ); } - context.logger.info( + logger.info( `${config ? 'Updating' : 'Creating'} config ${body.service.name}/${ body.service.environment }` ); - return await createOrUpdateConfiguration({ + await createOrUpdateConfiguration({ configurationId: config?._id, configurationIntake: body, setup, @@ -147,35 +153,35 @@ const searchParamsRt = t.intersection([ export type AgentConfigSearchParams = t.TypeOf; // Lookup single configuration (used by APM Server) -export const agentConfigurationSearchRoute = createRoute({ +const agentConfigurationSearchRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/agent-configuration/search', params: t.type({ body: searchParamsRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, logger } = resources; + const { service, etag, mark_as_applied_by_agent: markAsAppliedByAgent, - } = context.params.body; + } = params.body; - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const config = await searchConfigurations({ service, setup, }); if (!config) { - context.logger.debug( + logger.debug( `[Central configuration] Config was not found for ${service.name}/${service.environment}` ); throw Boom.notFound(); } - context.logger.info( - `Config was found for ${service.name}/${service.environment}` - ); + logger.info(`Config was found for ${service.name}/${service.environment}`); // update `applied_by_agent` field // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) @@ -197,11 +203,11 @@ export const agentConfigurationSearchRoute = createRoute({ */ // get list of services -export const listAgentConfigurationServicesRoute = createRoute({ +const listAgentConfigurationServicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/services', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -215,15 +221,17 @@ export const listAgentConfigurationServicesRoute = createRoute({ }); // get environments for service -export const listAgentConfigurationEnvironmentsRoute = createRoute({ +const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/environments', params: t.partial({ query: t.partial({ serviceName: t.string }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -239,16 +247,27 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({ }); // get agentName for service -export const agentConfigurationAgentNameRoute = createRoute({ +const agentConfigurationAgentNameRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', params: t.type({ query: t.type({ serviceName: t.string }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.query; const agentName = await getAgentNameByService({ serviceName, setup }); return { agentName }; }, }); + +export const agentConfigurationRouteRepository = createApmServerRouteRepository() + .add(agentConfigurationRoute) + .add(getSingleAgentConfigurationRoute) + .add(deleteAgentConfigurationRoute) + .add(createOrUpdateAgentConfigurationRoute) + .add(agentConfigurationSearchRoute) + .add(listAgentConfigurationServicesRoute) + .add(listAgentConfigurationEnvironmentsRoute) + .add(agentConfigurationAgentNameRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index de7f35c4081bc..98467e1a4a0dd 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; @@ -18,15 +18,17 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; import { notifyFeatureUsage } from '../../feature'; import { withApmSpan } from '../../utils/with_apm_span'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; // get ML anomaly detection jobs for each environment -export const anomalyDetectionJobsRoute = createRoute({ +const anomalyDetectionJobsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:ml:canGetJobs'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { context, logger } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); @@ -34,7 +36,7 @@ export const anomalyDetectionJobsRoute = createRoute({ const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () => Promise.all([ - getAnomalyDetectionJobs(setup, context.logger), + getAnomalyDetectionJobs(setup, logger), hasLegacyJobs(setup), ]) ); @@ -47,7 +49,7 @@ export const anomalyDetectionJobsRoute = createRoute({ }); // create new ML anomaly detection jobs for each given environment -export const createAnomalyDetectionJobsRoute = createRoute({ +const createAnomalyDetectionJobsRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'], @@ -57,15 +59,17 @@ export const createAnomalyDetectionJobsRoute = createRoute({ environments: t.array(t.string), }), }), - handler: async ({ context, request }) => { - const { environments } = context.params.body; - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params, context, logger } = resources; + const { environments } = params.body; + + const setup = await setupRequest(resources); if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } - await createAnomalyDetectionJobs(setup, environments, context.logger); + await createAnomalyDetectionJobs(setup, environments, logger); notifyFeatureUsage({ licensingPlugin: context.licensing, @@ -77,11 +81,11 @@ export const createAnomalyDetectionJobsRoute = createRoute({ }); // get all available environments to create anomaly detection jobs for -export const anomalyDetectionEnvironmentsRoute = createRoute({ +const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/anomaly-detection/environments', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -96,3 +100,8 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({ return { environments }; }, }); + +export const anomalyDetectionRouteRepository = createApmServerRouteRepository() + .add(anomalyDetectionJobsRoute) + .add(createAnomalyDetectionJobsRoute) + .add(anomalyDetectionEnvironmentsRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 91057c97579e4..003471aa89f39 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { createRoute } from '../create_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getApmIndices, getApmIndexSettings, @@ -14,29 +15,30 @@ import { import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; // get list of apm indices and values -export const apmIndexSettingsRoute = createRoute({ +const apmIndexSettingsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/apm-index-settings', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { - const apmIndexSettings = await getApmIndexSettings({ context }); + handler: async ({ config, context }) => { + const apmIndexSettings = await getApmIndexSettings({ config, context }); return { apmIndexSettings }; }, }); // get apm indices configuration object -export const apmIndicesRoute = createRoute({ +const apmIndicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/apm-indices', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { + handler: async (resources) => { + const { context, config } = resources; return await getApmIndices({ savedObjectsClient: context.core.savedObjects.client, - config: context.config, + config, }); }, }); // save ui indices -export const saveApmIndicesRoute = createRoute({ +const saveApmIndicesRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/apm-indices/save', options: { tags: ['access:apm', 'access:apm_write'], @@ -53,9 +55,15 @@ export const saveApmIndicesRoute = createRoute({ /* eslint-enable @typescript-eslint/naming-convention */ }), }), - handler: async ({ context }) => { - const { body } = context.params; + handler: async (resources) => { + const { params, context } = resources; + const { body } = params; const savedObjectsClient = context.core.savedObjects.client; return await saveApmIndices(savedObjectsClient, body); }, }); + +export const apmIndicesRouteRepository = createApmServerRouteRepository() + .add(apmIndexSettingsRoute) + .add(apmIndicesRoute) + .add(saveApmIndicesRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index a6ab553f09419..c9c5d236c14f9 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -21,35 +21,40 @@ import { import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link'; import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; -export const customLinkTransactionRoute = createRoute({ +const customLinkTransactionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/custom_links/transaction', options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { query } = context.params; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { query } = params; // picks only the items listed in FILTER_OPTIONS const filters = pick(query, FILTER_OPTIONS); return await getTransaction({ setup, filters }); }, }); -export const listCustomLinksRoute = createRoute({ +const listCustomLinksRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/custom_links', options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { query } = context.params; + const setup = await setupRequest(resources); + + const { query } = params; + // picks only the items listed in FILTER_OPTIONS const filters = pick(query, FILTER_OPTIONS); const customLinks = await listCustomLinks({ setup, filters }); @@ -57,29 +62,30 @@ export const listCustomLinksRoute = createRoute({ }, }); -export const createCustomLinkRoute = createRoute({ +const createCustomLinkRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/custom_links', params: t.type({ body: payloadRt, }), options: { tags: ['access:apm', 'access:apm_write'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const customLink = context.params.body; - const res = await createOrUpdateCustomLink({ customLink, setup }); + const setup = await setupRequest(resources); + const customLink = params.body; notifyFeatureUsage({ licensingPlugin: context.licensing, featureName: 'customLinks', }); - return res; + + await createOrUpdateCustomLink({ customLink, setup }); }, }); -export const updateCustomLinkRoute = createRoute({ +const updateCustomLinkRoute = createApmServerRoute({ endpoint: 'PUT /api/apm/settings/custom_links/{id}', params: t.type({ path: t.type({ @@ -90,23 +96,26 @@ export const updateCustomLinkRoute = createRoute({ options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, context } = resources; + if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { id } = context.params.path; - const customLink = context.params.body; - const res = await createOrUpdateCustomLink({ + const setup = await setupRequest(resources); + + const { id } = params.path; + const customLink = params.body; + + await createOrUpdateCustomLink({ customLinkId: id, customLink, setup, }); - return res; }, }); -export const deleteCustomLinkRoute = createRoute({ +const deleteCustomLinkRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/settings/custom_links/{id}', params: t.type({ path: t.type({ @@ -116,12 +125,14 @@ export const deleteCustomLinkRoute = createRoute({ options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; + if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { id } = context.params.path; + const setup = await setupRequest(resources); + const { id } = params.path; const res = await deleteCustomLink({ customLinkId: id, setup, @@ -129,3 +140,10 @@ export const deleteCustomLinkRoute = createRoute({ return res; }, }); + +export const customLinkRouteRepository = createApmServerRouteRepository() + .add(customLinkTransactionRoute) + .add(listCustomLinksRoute) + .add(createCustomLinkRoute) + .add(updateCustomLinkRoute) + .add(deleteCustomLinkRoute); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 6287ffbf0c751..dd392982b02fd 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -9,20 +9,22 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getTrace } from '../lib/traces/get_trace'; import { getTransactionGroupList } from '../lib/transaction_groups'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const tracesRoute = createRoute({ +const tracesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces', params: t.type({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { environment, kuery } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -34,7 +36,7 @@ export const tracesRoute = createRoute({ }, }); -export const tracesByIdRoute = createRoute({ +const tracesByIdRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces/{traceId}', params: t.type({ path: t.type({ @@ -43,13 +45,16 @@ export const tracesByIdRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - return getTrace(context.params.path.traceId, setup); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { traceId } = params.path; + return getTrace(traceId, setup); }, }); -export const rootTransactionByTraceIdRoute = createRoute({ +const rootTransactionByTraceIdRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', params: t.type({ path: t.type({ @@ -57,9 +62,15 @@ export const rootTransactionByTraceIdRoute = createRoute({ }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const { traceId } = context.params.path; - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params } = resources; + const { traceId } = params.path; + const setup = await setupRequest(resources); return getRootTransactionByTraceId(traceId, setup); }, }); + +export const traceRouteRepository = createApmServerRouteRepository() + .add(tracesByIdRoute) + .add(tracesRoute) + .add(rootTransactionByTraceIdRoute); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index f3424a252e409..ebca374db86d7 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { LatencyAggregationType, latencyAggregationTypeRt, } from '../../common/latency_aggregation_types'; -import { jsonRt } from '../../common/runtime_types/json_rt'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; @@ -23,7 +23,8 @@ import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, @@ -35,7 +36,7 @@ import { * Returns a list of transactions grouped by name * //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/ */ -export const transactionGroupsRoute = createRoute({ +const transactionGroupsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups', params: t.type({ path: t.type({ @@ -49,10 +50,11 @@ export const transactionGroupsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment, kuery, transactionType } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { environment, kuery, transactionType } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -72,7 +74,7 @@ export const transactionGroupsRoute = createRoute({ }, }); -export const transactionGroupsPrimaryStatisticsRoute = createRoute({ +const transactionGroupsPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics', params: t.type({ @@ -90,8 +92,9 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -100,7 +103,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ const { path: { serviceName }, query: { environment, kuery, latencyAggregationType, transactionType }, - } = context.params; + } = params; return getServiceTransactionGroups({ environment, @@ -109,12 +112,12 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ serviceName, searchAggregatedTransactions, transactionType, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, }); }, }); -export const transactionGroupsComparisonStatisticsRoute = createRoute({ +const transactionGroupsComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics', params: t.type({ @@ -135,13 +138,15 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); + const { params } = resources; + const { path: { serviceName }, query: { @@ -154,7 +159,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ comparisonStart, comparisonEnd, }, - } = context.params; + } = params; return await getServiceTransactionGroupComparisonStatisticsPeriods({ environment, @@ -165,14 +170,14 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ searchAggregatedTransactions, transactionType, numBuckets, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, comparisonStart, comparisonEnd, }); }, }); -export const transactionLatencyChartsRoute = createRoute({ +const transactionLatencyChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency', params: t.type({ path: t.type({ @@ -188,10 +193,11 @@ export const transactionLatencyChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const logger = context.logger; - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { serviceName } = params.path; const { environment, kuery, @@ -200,7 +206,7 @@ export const transactionLatencyChartsRoute = createRoute({ latencyAggregationType, comparisonStart, comparisonEnd, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -242,7 +248,7 @@ export const transactionLatencyChartsRoute = createRoute({ }, }); -export const transactionThroughputChartsRoute = createRoute({ +const transactionThroughputChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', params: t.type({ @@ -258,15 +264,17 @@ export const transactionThroughputChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.path; const { environment, kuery, transactionType, transactionName, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -284,7 +292,7 @@ export const transactionThroughputChartsRoute = createRoute({ }, }); -export const transactionChartsDistributionRoute = createRoute({ +const transactionChartsDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', params: t.type({ @@ -306,9 +314,10 @@ export const transactionChartsDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, @@ -316,7 +325,7 @@ export const transactionChartsDistributionRoute = createRoute({ transactionName, transactionId = '', traceId = '', - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -336,7 +345,7 @@ export const transactionChartsDistributionRoute = createRoute({ }, }); -export const transactionChartsBreakdownRoute = createRoute({ +const transactionChartsBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown', params: t.type({ path: t.type({ @@ -351,15 +360,17 @@ export const transactionChartsBreakdownRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.path; const { environment, kuery, transactionName, transactionType, - } = context.params.query; + } = params.query; return getTransactionBreakdown({ environment, @@ -372,7 +383,7 @@ export const transactionChartsBreakdownRoute = createRoute({ }, }); -export const transactionChartsErrorRateRoute = createRoute({ +const transactionChartsErrorRateRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/error_rate', params: t.type({ @@ -386,9 +397,10 @@ export const transactionChartsErrorRateRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; const { serviceName } = params.path; const { environment, @@ -416,3 +428,13 @@ export const transactionChartsErrorRateRoute = createRoute({ }); }, }); + +export const transactionRouteRepository = createApmServerRouteRepository() + .add(transactionGroupsRoute) + .add(transactionGroupsPrimaryStatisticsRoute) + .add(transactionGroupsComparisonStatisticsRoute) + .add(transactionLatencyChartsRoute) + .add(transactionThroughputChartsRoute) + .add(transactionChartsDistributionRoute) + .add(transactionChartsBreakdownRoute) + .add(transactionChartsErrorRateRoute); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 3ba24b4ed5268..0fec88a4326c3 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -5,27 +5,19 @@ * 2.0. */ -import t, { Encode, Encoder } from 'io-ts'; import { CoreSetup, - KibanaRequest, RequestHandlerContext, Logger, + KibanaRequest, + CoreStart, } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { RequiredKeys, DeepPartial } from 'utility-types'; -import { SpacesPluginStart } from '../../../spaces/server'; -import { ObservabilityPluginSetup } from '../../../observability/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; -import { SecurityPluginSetup } from '../../../security/server'; -import { MlPluginSetup } from '../../../ml/server'; -import { FetchOptions } from '../../common/fetch_options'; import { APMConfig } from '..'; +import { APMPluginDependencies } from '../types'; -export type HandlerReturn = Record; - -interface InspectQueryParam { - query: { _inspect: boolean }; +export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { + licensing: LicensingApiRequestHandlerContext; } export type InspectResponse = Array<{ @@ -36,141 +28,53 @@ export type InspectResponse = Array<{ esError: Error; }>; -export interface RouteParams { - path?: Record; - query?: Record; - body?: any; +export interface APMRouteCreateOptions { + options: { + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + >; + }; } -type WithoutIncompatibleMethods = Omit< - T, - 'encode' | 'asEncoder' -> & { encode: Encode; asEncoder: () => Encoder }; - -export type RouteParamsRT = WithoutIncompatibleMethods>; - -export type RouteHandler< - TParamsRT extends RouteParamsRT | undefined, - TReturn extends HandlerReturn -> = (kibanaContext: { - context: APMRequestHandlerContext< - (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) & - InspectQueryParam - >; +export interface APMRouteHandlerResources { request: KibanaRequest; -}) => Promise; - -interface RouteOptions { - tags: Array< - | 'access:apm' - | 'access:apm_write' - | 'access:ml:canGetJobs' - | 'access:ml:canCreateJob' - >; -} - -export interface Route< - TEndpoint extends string, - TRouteParamsRT extends RouteParamsRT | undefined, - TReturn extends HandlerReturn -> { - endpoint: TEndpoint; - options: RouteOptions; - params?: TRouteParamsRT; - handler: RouteHandler; -} - -/** - * @internal - */ -export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { - licensing: LicensingApiRequestHandlerContext; -} - -export type APMRequestHandlerContext< - TRouteParams = {} -> = ApmPluginRequestHandlerContext & { - params: TRouteParams & InspectQueryParam; + context: ApmPluginRequestHandlerContext; + params: { + query: { + _inspect: boolean; + }; + }; config: APMConfig; logger: Logger; - plugins: { - spaces?: SpacesPluginStart; - observability?: ObservabilityPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; + core: { + setup: CoreSetup; + start: () => Promise; }; -}; - -export interface RouteState { - [endpoint: string]: { - params?: RouteParams; - ret: any; + plugins: { + [key in keyof APMPluginDependencies]: { + setup: Required[key]['setup']; + start: () => Promise[key]['start']>; + }; }; } -export interface ServerAPI { - _S: TRouteState; - add< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined - >( - route: - | Route - | ((core: CoreSetup) => Route) - ): ServerAPI< - TRouteState & - { - [key in TEndpoint]: { - params: TRouteParamsRT; - ret: TReturn & { _inspect?: InspectResponse }; - }; - } - >; - init: ( - core: CoreSetup, - context: { - config$: Observable; - logger: Logger; - plugins: { - observability?: ObservabilityPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; - }; - } - ) => void; -} - -type MaybeOptional }> = RequiredKeys< - T['params'] -> extends never - ? { params?: T['params'] } - : { params: T['params'] }; - -export type MaybeParams< - TRouteState, - TEndpoint extends keyof TRouteState & string -> = TRouteState[TEndpoint] extends { params: t.Any } - ? MaybeOptional<{ - params: t.OutputOf & - DeepPartial; - }> - : {}; - -export type Client< - TRouteState, - TOptions extends { abortable: boolean } = { abortable: true } -> = ( - options: Omit< - FetchOptions, - 'query' | 'body' | 'pathname' | 'method' | 'signal' - > & { - forceCache?: boolean; - endpoint: TEndpoint; - } & MaybeParams & - (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {}) -) => Promise< - TRouteState[TEndpoint] extends { ret: any } - ? TRouteState[TEndpoint]['ret'] - : unknown ->; +// export type Client< +// TRouteState, +// TOptions extends { abortable: boolean } = { abortable: true } +// > = ( +// options: Omit< +// FetchOptions, +// 'query' | 'body' | 'pathname' | 'method' | 'signal' +// > & { +// forceCache?: boolean; +// endpoint: TEndpoint; +// } & MaybeParams & +// (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {}) +// ) => Promise< +// TRouteState[TEndpoint] extends { ret: any } +// ? TRouteState[TEndpoint]['ret'] +// : unknown +// >; diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts new file mode 100644 index 0000000000000..cef9eaf2f4fc0 --- /dev/null +++ b/x-pack/plugins/apm/server/types.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ValuesType } from 'utility-types'; +import { Observable } from 'rxjs'; +import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; +import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; +import { + HomeServerPluginSetup, + HomeServerPluginStart, +} from '../../../../src/plugins/home/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { ActionsPlugin } from '../../actions/server'; +import { AlertingPlugin } from '../../alerting/server'; +import { CloudSetup } from '../../cloud/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; +import { + LicensingPluginSetup, + LicensingPluginStart, +} from '../../licensing/server'; +import { MlPluginSetup, MlPluginStart } from '../../ml/server'; +import { ObservabilityPluginSetup } from '../../observability/server'; +import { + SecurityPluginSetup, + SecurityPluginStart, +} from '../../security/server'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../task_manager/server'; +import { APMConfig } from '.'; +import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; +import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; +import { ApmPluginRequestHandlerContext } from './routes/typings'; + +export interface APMPluginSetup { + config$: Observable; + getApmIndices: () => ReturnType; + createApmEventClient: (params: { + debug?: boolean; + request: KibanaRequest; + context: ApmPluginRequestHandlerContext; + }) => Promise>; +} + +interface DependencyMap { + core: { + setup: CoreSetup; + start: CoreStart; + }; + spaces: { + setup: SpacesPluginSetup; + start: SpacesPluginStart; + }; + apmOss: { + setup: APMOSSPluginSetup; + start: undefined; + }; + home: { + setup: HomeServerPluginSetup; + start: HomeServerPluginStart; + }; + licensing: { + setup: LicensingPluginSetup; + start: LicensingPluginStart; + }; + cloud: { + setup: CloudSetup; + start: undefined; + }; + usageCollection: { + setup: UsageCollectionSetup; + start: undefined; + }; + taskManager: { + setup: TaskManagerSetupContract; + start: TaskManagerStartContract; + }; + alerting: { + setup: AlertingPlugin['setup']; + start: AlertingPlugin['start']; + }; + actions: { + setup: ActionsPlugin['setup']; + start: ActionsPlugin['start']; + }; + observability: { + setup: ObservabilityPluginSetup; + start: undefined; + }; + features: { + setup: FeaturesPluginSetup; + start: FeaturesPluginStart; + }; + security: { + setup: SecurityPluginSetup; + start: SecurityPluginStart; + }; + ml: { + setup: MlPluginSetup; + start: MlPluginStart; + }; + data: { + setup: DataPluginSetup; + start: DataPluginStart; + }; +} + +const requiredDependencies = [ + 'features', + 'apmOss', + 'data', + 'licensing', + 'triggersActionsUi', + 'embeddable', + 'infra', +] as const; + +const optionalDependencies = [ + 'spaces', + 'cloud', + 'usageCollection', + 'taskManager', + 'actions', + 'alerting', + 'observability', + 'security', + 'ml', + 'home', + 'maps', +] as const; + +type RequiredDependencies = Pick< + DependencyMap, + ValuesType & keyof DependencyMap +>; + +type OptionalDependencies = Partial< + Pick< + DependencyMap, + ValuesType & keyof DependencyMap + > +>; + +export type APMPluginDependencies = RequiredDependencies & OptionalDependencies; + +export type APMPluginSetupDependencies = { + [key in keyof APMPluginDependencies]: Required[key]['setup']; +}; + +export type APMPluginStartDependencies = { + [key in keyof APMPluginDependencies]: Required[key]['start']; +}; diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts index 542982778dfff..ed104a6fdf064 100644 --- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -8,24 +8,25 @@ import { format } from 'url'; import supertest from 'supertest'; import request from 'superagent'; -import { MaybeParams } from '../../../plugins/apm/server/routes/typings'; import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint'; -import { APMAPI } from '../../../plugins/apm/server/routes/create_apm_api'; -import type { APIReturnType } from '../../../plugins/apm/public/services/rest/createCallApmApi'; +import type { + APIReturnType, + APIEndpoint, + APIClientRequestParamsOf, +} from '../../../plugins/apm/public/services/rest/createCallApmApi'; export function createApmApiSupertest(st: supertest.SuperTest) { - return async ( + return async ( options: { - endpoint: TPath; - } & MaybeParams + endpoint: TEndpoint; + } & APIClientRequestParamsOf & { params?: { query?: { _inspect?: boolean } } } ): Promise<{ status: number; - body: APIReturnType; + body: APIReturnType; }> => { const { endpoint } = options; - // @ts-expect-error - const params = 'params' in options ? options.params : {}; + const params = 'params' in options ? (options.params as Record) : {}; const { method, pathname } = parseEndpoint(endpoint, params?.path); const url = format({ pathname, query: params?.query }); diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts index aae2e38e8ec8e..4f65808de820e 100644 --- a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts +++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts @@ -81,7 +81,6 @@ export default function customLinksTests({ getService }: FtrProviderContext) { it('for agent configs', async () => { const { status, body } = await supertestRead({ endpoint: 'GET /api/apm/settings/agent-configuration', - // @ts-expect-error params: { query: { _inspect: true, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts index aac92685a3c34..baa95eb56a126 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; import { createApmApiSupertest } from '../../common/apm_api_supertest'; +import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiSupertest = createApmApiSupertest(getService('supertest')); @@ -31,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { params: { path: { serviceName: 'opbeans-java' }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, start, end, transactionType: 'request', @@ -61,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { params: { path: { serviceName: 'opbeans-java' }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, start, end, transactionType: 'request', @@ -130,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { params: { path: { serviceName: 'opbeans-ruby' }, query: { - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, start, end, transactionType: 'request', diff --git a/yarn.lock b/yarn.lock index bb5d9ff8c23aa..786143fb3d1ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2680,6 +2680,10 @@ version "0.0.0" uid "" +"@kbn/io-ts-utils@link:packages/kbn-io-ts-utils": + version "0.0.0" + uid "" + "@kbn/legacy-logging@link:packages/kbn-legacy-logging": version "0.0.0" uid "" @@ -2712,6 +2716,10 @@ version "0.0.0" uid "" +"@kbn/server-route-repository@link:packages/kbn-server-route-repository": + version "0.0.0" + uid "" + "@kbn/std@link:packages/kbn-std": version "0.0.0" uid "" From 03a51f4eec497ff1d798a4bcb1d00ddc91305645 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Thu, 8 Apr 2021 07:47:29 -0400 Subject: [PATCH 107/131] [Alerting] Update feature privilege display names (#96083) * Updating feature display names * Updating feature display names --- x-pack/examples/alerting_example/server/plugin.ts | 2 +- x-pack/plugins/stack_alerts/server/feature.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts index db9c996147c94..f6131679874db 100644 --- a/x-pack/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -33,7 +33,7 @@ export class AlertingExamplePlugin implements Plugin Date: Thu, 8 Apr 2021 14:18:18 +0200 Subject: [PATCH 108/131] [Remote clusters] Cloud deployment form when adding new cluster (#94450) * Implemented in-form Cloud deployment url input * Fixed i18n files and added mode switch back for non-Cloud * Added cloud docs link to the documentation service, fixed snapshot tests * Fixed docs build * Added jest test for the new cloud url input * Added unit test for cloud validation * Fixed eslint error * Fixed ts errors * Added ts-ignore * Fixed ts errors * Refactored connection mode component and component tests * Fixed import * Fixed copy * Fixed copy * Reverted docs changes * Reverted docs changes * Replaced the screenshot with a popover and refactored integration tests * Added todo for cloud deployments link * Changed cloud URL help text copy * Added cloud base url and deleted unnecessary base path * Fixed es error * Fixed es error * Changed wording * Reverted docs changes * Updated the help popover * Deleted unneeded fragment component * Deleted unneeded fragment component * Updated tests descriptions to be more detailed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../add/remote_clusters_add.helpers.js | 43 - .../add/remote_clusters_add.helpers.tsx | 47 + .../add/remote_clusters_add.test.js | 230 -- .../add/remote_clusters_add.test.ts | 260 +++ ...al_characters.js => special_characters.ts} | 0 .../edit/remote_clusters_edit.helpers.js | 34 - .../edit/remote_clusters_edit.helpers.tsx | 58 + .../edit/remote_clusters_edit.test.js | 80 - .../edit/remote_clusters_edit.test.tsx | 141 ++ .../{http_requests.js => http_requests.ts} | 19 +- .../helpers/{index.js => index.ts} | 1 + .../helpers/remote_clusters_actions.ts | 199 ++ ...up_environment.js => setup_environment.ts} | 2 + .../public/application/app_context.tsx | 10 +- .../public/application/index.d.ts | 3 +- .../remote_cluster_form.test.js.snap | 2017 ----------------- .../components/cloud_url_help.tsx | 61 + .../components/connection_mode.tsx | 99 + .../remote_cluster_form/components/index.ts | 8 + .../components/proxy_connection.tsx | 162 ++ .../components/sniff_connection.tsx | 158 ++ .../{index.js => index.ts} | 0 .../remote_cluster_form.js | 962 -------- .../remote_cluster_form.test.js | 53 - .../remote_cluster_form.tsx | 629 +++++ .../remote_cluster_form/request_flyout.tsx | 4 +- .../remote_cluster_form/validators/index.ts | 7 + .../validators/validate_cloud_url.test.ts | 128 ++ .../validators/validate_cloud_url.tsx | 80 + .../validators/validate_cluster.tsx | 39 + .../validators/validate_seed.ts | 43 - .../validators/validate_seed.tsx | 40 + .../public/application/sections/index.d.ts | 11 + .../remote_cluster_add/remote_cluster_add.js | 2 +- .../remote_cluster_edit.js | 7 +- .../public/application/services/api.ts | 2 +- .../application/services/documentation.ts | 10 +- .../public/application/services/index.ts | 8 +- .../public/application/services/routing.ts | 2 +- .../public/application/store/index.d.ts | 11 + .../public/assets/cloud_screenshot.png | Bin 0 -> 197089 bytes .../plugins/remote_clusters/public/plugin.ts | 5 +- x-pack/plugins/remote_clusters/tsconfig.json | 2 + .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 45 files changed, 2184 insertions(+), 3503 deletions(-) delete mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js create mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx delete mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js create mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts rename x-pack/plugins/remote_clusters/__jest__/client_integration/add/{special_characters.js => special_characters.ts} (100%) delete mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js create mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx delete mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js create mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx rename x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/{http_requests.js => http_requests.ts} (63%) rename x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/{index.js => index.ts} (80%) create mode 100644 x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts rename x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/{setup_environment.js => setup_environment.ts} (95%) delete mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx rename x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js delete mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx delete mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/index.d.ts create mode 100644 x-pack/plugins/remote_clusters/public/application/store/index.d.ts create mode 100644 x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js deleted file mode 100644 index 38672b4d59a20..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { registerTestBed } from '@kbn/test/jest'; - -import { RemoteClusterAdd } from '../../../public/application/sections/remote_cluster_add'; -import { createRemoteClustersStore } from '../../../public/application/store'; -import { registerRouter } from '../../../public/application/services/routing'; - -const testBedConfig = { - store: createRemoteClustersStore, - memoryRouter: { - onRouter: (router) => registerRouter(router), - }, -}; - -const initTestBed = registerTestBed(RemoteClusterAdd, testBedConfig); - -export const setup = (props) => { - const testBed = initTestBed(props); - - // User actions - const clickSaveForm = async () => { - await act(async () => { - testBed.find('remoteClusterFormSaveButton').simulate('click'); - }); - - testBed.component.update(); - }; - - return { - ...testBed, - actions: { - clickSaveForm, - }, - }; -}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx new file mode 100644 index 0000000000000..a47e6c023a161 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { registerTestBed } from '@kbn/test/jest'; + +import { RemoteClusterAdd } from '../../../public/application/sections'; +import { createRemoteClustersStore } from '../../../public/application/store'; +import { AppRouter, registerRouter } from '../../../public/application/services'; +import { createRemoteClustersActions } from '../helpers'; +import { AppContextProvider } from '../../../public/application/app_context'; + +const ComponentWithContext = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { + return ( + + + + ); +}; + +const testBedConfig = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { + return { + store: createRemoteClustersStore, + memoryRouter: { + onRouter: (router: AppRouter) => registerRouter(router), + }, + defaultProps: { isCloudEnabled }, + }; +}; + +const initTestBed = (isCloudEnabled: boolean) => + registerTestBed(ComponentWithContext, testBedConfig({ isCloudEnabled }))(); + +export const setup = async (isCloudEnabled = false) => { + const testBed = await initTestBed(isCloudEnabled); + + return { + ...testBed, + actions: { + ...createRemoteClustersActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js deleted file mode 100644 index 40abde35835f0..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { setupEnvironment } from '../helpers'; -import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters'; -import { setup } from './remote_clusters_add.helpers'; - -describe('Create Remote cluster', () => { - describe('on component mount', () => { - let find; - let exists; - let actions; - let form; - let server; - let component; - - beforeAll(() => { - ({ server } = setupEnvironment()); - }); - - afterAll(() => { - server.restore(); - }); - - beforeEach(async () => { - await act(async () => { - ({ form, exists, find, actions, component } = setup()); - }); - component.update(); - }); - - test('should have the title of the page set correctly', () => { - expect(exists('remoteClusterPageTitle')).toBe(true); - expect(find('remoteClusterPageTitle').text()).toEqual('Add remote cluster'); - }); - - test('should have a link to the documentation', () => { - expect(exists('remoteClusterDocsButton')).toBe(true); - }); - - test('should have a toggle to Skip unavailable remote cluster', () => { - expect(exists('remoteClusterFormSkipUnavailableFormToggle')).toBe(true); - - // By default it should be set to "false" - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe( - false - ); - - act(() => { - form.toggleEuiSwitch('remoteClusterFormSkipUnavailableFormToggle'); - }); - - component.update(); - - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true); - }); - - test('should have a toggle to enable "proxy" mode for a remote cluster', () => { - expect(exists('remoteClusterFormConnectionModeToggle')).toBe(true); - - // By default it should be set to "false" - expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false); - - act(() => { - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); - }); - - component.update(); - - expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true); - }); - - test('should display errors and disable the save button when clicking "save" without filling the form', async () => { - expect(exists('remoteClusterFormGlobalError')).toBe(false); - expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false); - - await actions.clickSaveForm(); - - expect(exists('remoteClusterFormGlobalError')).toBe(true); - expect(form.getErrorsMessages()).toEqual([ - 'Name is required.', - 'At least one seed node is required.', - ]); - expect(find('remoteClusterFormSaveButton').props().disabled).toBe(true); - }); - }); - - describe('form validation', () => { - describe('remote cluster name', () => { - let component; - let actions; - let form; - - beforeEach(async () => { - await act(async () => { - ({ component, form, actions } = setup()); - }); - - component.update(); - }); - - test('should not allow spaces', async () => { - form.setInputValue('remoteClusterFormNameInput', 'with space'); - - await actions.clickSaveForm(); - - expect(form.getErrorsMessages()).toContain('Spaces are not allowed in the name.'); - }); - - test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => { - const expectInvalidChar = (char) => { - if (char === '-' || char === '_') { - return; - } - - try { - form.setInputValue('remoteClusterFormNameInput', `with${char}`); - - expect(form.getErrorsMessages()).toContain( - `Remove the character ${char} from the name.` - ); - } catch { - throw Error(`Char "${char}" expected invalid but was allowed`); - } - }; - - await actions.clickSaveForm(); // display form errors - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar); - }); - }); - - describe('seeds', () => { - let actions; - let form; - let component; - - beforeEach(async () => { - await act(async () => { - ({ form, actions, component } = setup()); - }); - - component.update(); - - form.setInputValue('remoteClusterFormNameInput', 'remote_cluster_test'); - }); - - test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => { - await actions.clickSaveForm(); // display form errors - - const notInArray = (array) => (value) => array.indexOf(value) < 0; - - const expectInvalidChar = (char) => { - form.setComboBoxValue('remoteClusterFormSeedsInput', `192.16${char}:3000`); - expect(form.getErrorsMessages()).toContain( - `Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.` - ); - }; - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] - .filter(notInArray(['-', '_', ':'])) - .forEach(expectInvalidChar); - }); - - test('should require a numeric "port" to be set', async () => { - await actions.clickSaveForm(); - - form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - - form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1:abc'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - }); - }); - - describe('proxy address', () => { - let actions; - let form; - let component; - - beforeEach(async () => { - await act(async () => { - ({ form, actions, component } = setup()); - }); - - component.update(); - - act(() => { - // Enable "proxy" mode - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); - }); - - component.update(); - }); - - test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => { - await actions.clickSaveForm(); // display form errors - - const notInArray = (array) => (value) => array.indexOf(value) < 0; - - const expectInvalidChar = (char) => { - form.setInputValue('remoteClusterFormProxyAddressInput', `192.16${char}:3000`); - expect(form.getErrorsMessages()).toContain( - 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' - ); - }; - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] - .filter(notInArray(['-', '_', ':'])) - .forEach(expectInvalidChar); - }); - - test('should require a numeric "port" to be set', async () => { - await actions.clickSaveForm(); - - form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - - form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1:abc'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - }); - }); - }); -}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts new file mode 100644 index 0000000000000..0727bc0c9ba2d --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SinonFakeServer } from 'sinon'; +import { TestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, RemoteClustersActions } from '../helpers'; +import { setup } from './remote_clusters_add.helpers'; +import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters'; + +const notInArray = (array: string[]) => (value: string) => array.indexOf(value) < 0; + +let component: TestBed['component']; +let actions: RemoteClustersActions; +let server: SinonFakeServer; + +describe('Create Remote cluster', () => { + beforeAll(() => { + ({ server } = setupEnvironment()); + }); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + component.update(); + }); + + describe('on component mount', () => { + test('should have the title of the page set correctly', () => { + expect(actions.pageTitle.exists()).toBe(true); + expect(actions.pageTitle.text()).toEqual('Add remote cluster'); + }); + + test('should have a link to the documentation', () => { + expect(actions.docsButtonExists()).toBe(true); + }); + + test('should have a toggle to Skip unavailable remote cluster', () => { + expect(actions.skipUnavailableSwitch.exists()).toBe(true); + + // By default it should be set to "false" + expect(actions.skipUnavailableSwitch.isChecked()).toBe(false); + + actions.skipUnavailableSwitch.toggle(); + + expect(actions.skipUnavailableSwitch.isChecked()).toBe(true); + }); + + describe('on prem', () => { + test('should have a toggle to enable "proxy" mode for a remote cluster', () => { + expect(actions.connectionModeSwitch.exists()).toBe(true); + + // By default it should be set to "false" + expect(actions.connectionModeSwitch.isChecked()).toBe(false); + + actions.connectionModeSwitch.toggle(); + + expect(actions.connectionModeSwitch.isChecked()).toBe(true); + }); + + test('server name has optional label', () => { + actions.connectionModeSwitch.toggle(); + expect(actions.serverNameInput.getLabel()).toBe('Server name (optional)'); + }); + + test('should display errors and disable the save button when clicking "save" without filling the form', async () => { + expect(actions.globalErrorExists()).toBe(false); + expect(actions.saveButton.isDisabled()).toBe(false); + + await actions.saveButton.click(); + + expect(actions.globalErrorExists()).toBe(true); + expect(actions.getErrorMessages()).toEqual([ + 'Name is required.', + // seeds input is switched on by default on prem and is required + 'At least one seed node is required.', + ]); + expect(actions.saveButton.isDisabled()).toBe(true); + }); + + test('renders no switch for cloud url input and proxy address + server name input modes', () => { + expect(actions.cloudUrlSwitch.exists()).toBe(false); + }); + }); + describe('on cloud', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup(true)); + }); + + component.update(); + }); + + test('renders a switch between cloud url input and proxy address + server name input for proxy connection', () => { + expect(actions.cloudUrlSwitch.exists()).toBe(true); + }); + + test('renders no switch between sniff and proxy modes', () => { + expect(actions.connectionModeSwitch.exists()).toBe(false); + }); + test('defaults to cloud url input for proxy connection', () => { + expect(actions.cloudUrlSwitch.isChecked()).toBe(false); + }); + test('server name has no optional label', () => { + actions.cloudUrlSwitch.toggle(); + expect(actions.serverNameInput.getLabel()).toBe('Server name'); + }); + }); + }); + describe('form validation', () => { + describe('remote cluster name', () => { + test('should not allow spaces', async () => { + actions.nameInput.setValue('with space'); + + await actions.saveButton.click(); + + expect(actions.getErrorMessages()).toContain('Spaces are not allowed in the name.'); + }); + + test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => { + const expectInvalidChar = (char: string) => { + if (char === '-' || char === '_') { + return; + } + + try { + actions.nameInput.setValue(`with${char}`); + + expect(actions.getErrorMessages()).toContain( + `Remove the character ${char} from the name.` + ); + } catch { + throw Error(`Char "${char}" expected invalid but was allowed`); + } + }; + + await actions.saveButton.click(); // display form errors + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar); + }); + }); + + describe('proxy address', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + + component.update(); + + actions.connectionModeSwitch.toggle(); + }); + + test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => { + await actions.saveButton.click(); // display form errors + + const expectInvalidChar = (char: string) => { + actions.proxyAddressInput.setValue(`192.16${char}:3000`); + expect(actions.getErrorMessages()).toContain( + 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', async () => { + await actions.saveButton.click(); + + actions.proxyAddressInput.setValue('192.168.1.1'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + + actions.proxyAddressInput.setValue('192.168.1.1:abc'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + }); + }); + + describe('on prem', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + + component.update(); + + actions.nameInput.setValue('remote_cluster_test'); + }); + + describe('seeds', () => { + test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => { + await actions.saveButton.click(); // display form errors + + const expectInvalidChar = (char: string) => { + actions.seedsInput.setValue(`192.16${char}:3000`); + expect(actions.getErrorMessages()).toContain( + `Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.` + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', async () => { + await actions.saveButton.click(); + + actions.seedsInput.setValue('192.168.1.1'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + + actions.seedsInput.setValue('192.168.1.1:abc'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + }); + }); + + test('server name is optional (proxy connection)', () => { + actions.connectionModeSwitch.toggle(); + actions.saveButton.click(); + expect(actions.getErrorMessages()).toEqual(['A proxy address is required.']); + }); + }); + + describe('on cloud', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup(true)); + }); + + component.update(); + }); + + test('cloud url is required since cloud url input is enabled by default', () => { + actions.saveButton.click(); + expect(actions.getErrorMessages()).toContain('A url is required.'); + }); + + test('proxy address and server name are required when cloud url input is disabled', () => { + actions.cloudUrlSwitch.toggle(); + actions.saveButton.click(); + expect(actions.getErrorMessages()).toEqual([ + 'Name is required.', + 'A proxy address is required.', + 'A server name is required.', + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.ts similarity index 100% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.ts diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js deleted file mode 100644 index 094fb5056e983..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { registerTestBed } from '@kbn/test/jest'; - -import { RemoteClusterEdit } from '../../../public/application/sections/remote_cluster_edit'; -import { createRemoteClustersStore } from '../../../public/application/store'; -import { registerRouter } from '../../../public/application/services/routing'; - -export const REMOTE_CLUSTER_EDIT_NAME = 'new-york'; - -export const REMOTE_CLUSTER_EDIT = { - name: REMOTE_CLUSTER_EDIT_NAME, - seeds: ['localhost:9400'], - skipUnavailable: true, -}; - -const testBedConfig = { - store: createRemoteClustersStore, - memoryRouter: { - onRouter: (router) => registerRouter(router), - // The remote cluster name to edit is read from the router ":id" param - // so we first set it in our initial entries - initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`], - // and then we declarae the :id param on the component route path - componentRoutePath: '/:name', - }, -}; - -export const setup = registerTestBed(RemoteClusterEdit, testBedConfig); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx new file mode 100644 index 0000000000000..2259396bf33f2 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; + +import React from 'react'; +import { RemoteClusterEdit } from '../../../public/application/sections'; +import { createRemoteClustersStore } from '../../../public/application/store'; +import { AppRouter, registerRouter } from '../../../public/application/services'; +import { createRemoteClustersActions } from '../helpers'; +import { AppContextProvider } from '../../../public/application/app_context'; + +export const REMOTE_CLUSTER_EDIT_NAME = 'new-york'; + +export const REMOTE_CLUSTER_EDIT = { + name: REMOTE_CLUSTER_EDIT_NAME, + seeds: ['localhost:9400'], + skipUnavailable: true, +}; + +const ComponentWithContext = (props: { isCloudEnabled: boolean }) => { + const { isCloudEnabled, ...rest } = props; + return ( + + + + ); +}; + +const testBedConfig: TestBedConfig = { + store: createRemoteClustersStore, + memoryRouter: { + onRouter: (router: AppRouter) => registerRouter(router), + // The remote cluster name to edit is read from the router ":id" param + // so we first set it in our initial entries + initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`], + // and then we declare the :id param on the component route path + componentRoutePath: '/:name', + }, +}; + +const initTestBed = (isCloudEnabled: boolean) => + registerTestBed(ComponentWithContext, testBedConfig)({ isCloudEnabled }); + +export const setup = async (isCloudEnabled = false) => { + const testBed = await initTestBed(isCloudEnabled); + + return { + ...testBed, + actions: { + ...createRemoteClustersActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js deleted file mode 100644 index 19dd468cb76c5..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form'; -import { setupEnvironment } from '../helpers'; -import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers'; -import { - setup, - REMOTE_CLUSTER_EDIT, - REMOTE_CLUSTER_EDIT_NAME, -} from './remote_clusters_edit.helpers'; - -describe('Edit Remote cluster', () => { - let component; - let find; - let exists; - - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); - - beforeEach(async () => { - await act(async () => { - ({ component, find, exists } = setup()); - }); - component.update(); - }); - - test('should have the title of the page set correctly', () => { - expect(exists('remoteClusterPageTitle')).toBe(true); - expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster'); - }); - - test('should have a link to the documentation', () => { - expect(exists('remoteClusterDocsButton')).toBe(true); - }); - - /** - * As the "edit" remote cluster component uses the same form underneath that - * the "create" remote cluster, we won't test it again but simply make sure that - * the form component is indeed shared between the 2 app sections. - */ - test('should use the same Form component as the "" component', async () => { - let addRemoteClusterTestBed; - - await act(async () => { - addRemoteClusterTestBed = setupRemoteClustersAdd(); - }); - - addRemoteClusterTestBed.component.update(); - - const formEdit = component.find(RemoteClusterForm); - const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm); - - expect(formEdit.length).toBe(1); - expect(formAdd.length).toBe(1); - }); - - test('should populate the form fields with the values from the remote cluster loaded', () => { - expect(find('remoteClusterFormNameInput').props().value).toBe(REMOTE_CLUSTER_EDIT_NAME); - expect(find('remoteClusterFormSeedsInput').text()).toBe(REMOTE_CLUSTER_EDIT.seeds.join('')); - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe( - REMOTE_CLUSTER_EDIT.skipUnavailable - ); - }); - - test('should disable the form name input', () => { - expect(find('remoteClusterFormNameInput').props().disabled).toBe(true); - }); -}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx new file mode 100644 index 0000000000000..2913de94bc2dd --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form'; +import { RemoteClustersActions, setupEnvironment } from '../helpers'; +import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers'; +import { + setup, + REMOTE_CLUSTER_EDIT, + REMOTE_CLUSTER_EDIT_NAME, +} from './remote_clusters_edit.helpers'; +import { Cluster } from '../../../common/lib'; + +let component: TestBed['component']; +let actions: RemoteClustersActions; +const { server, httpRequestsMockHelpers } = setupEnvironment(); + +describe('Edit Remote cluster', () => { + afterAll(() => { + server.restore(); + }); + + httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); + + beforeEach(async () => { + await act(async () => { + ({ component, actions } = await setup()); + }); + component.update(); + }); + + test('should have the title of the page set correctly', () => { + expect(actions.pageTitle.exists()).toBe(true); + expect(actions.pageTitle.text()).toEqual('Edit remote cluster'); + }); + + test('should have a link to the documentation', () => { + expect(actions.docsButtonExists()).toBe(true); + }); + + /** + * As the "edit" remote cluster component uses the same form underneath that + * the "create" remote cluster, we won't test it again but simply make sure that + * the form component is indeed shared between the 2 app sections. + */ + test('should use the same Form component as the "" component', async () => { + let addRemoteClusterTestBed: TestBed; + + await act(async () => { + addRemoteClusterTestBed = await setupRemoteClustersAdd(); + }); + + addRemoteClusterTestBed!.component.update(); + + const formEdit = component.find(RemoteClusterForm); + const formAdd = addRemoteClusterTestBed!.component.find(RemoteClusterForm); + + expect(formEdit.length).toBe(1); + expect(formAdd.length).toBe(1); + }); + + test('should populate the form fields with the values from the remote cluster loaded', () => { + expect(actions.nameInput.getValue()).toBe(REMOTE_CLUSTER_EDIT_NAME); + // seeds input for sniff connection is not shown on Cloud + expect(actions.seedsInput.getValue()).toBe(REMOTE_CLUSTER_EDIT.seeds.join('')); + expect(actions.skipUnavailableSwitch.isChecked()).toBe(REMOTE_CLUSTER_EDIT.skipUnavailable); + }); + + test('should disable the form name input', () => { + expect(actions.nameInput.isDisabled()).toBe(true); + }); + + describe('on cloud', () => { + const cloudUrl = 'cloud-url'; + const defaultCloudPort = '9400'; + test('existing cluster that defaults to cloud url (default port)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:${defaultCloudPort}`, + serverName: cloudUrl, + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(true); + expect(actions.cloudUrlInput.getValue()).toBe(cloudUrl); + }); + + test('existing cluster that defaults to manual input (non-default port)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:9500`, + serverName: cloudUrl, + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(false); + + expect(actions.proxyAddressInput.exists()).toBe(true); + expect(actions.serverNameInput.exists()).toBe(true); + }); + + test('existing cluster that defaults to manual input (proxy address is different from server name)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:${defaultCloudPort}`, + serverName: 'another-value', + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(false); + + expect(actions.proxyAddressInput.exists()).toBe(true); + expect(actions.serverNameInput.exists()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts similarity index 63% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts index 304ec51986aba..3ebe3ab5738d6 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts @@ -5,25 +5,24 @@ * 2.0. */ -import sinon from 'sinon'; +import sinon, { SinonFakeServer } from 'sinon'; +import { Cluster } from '../../../common/lib'; // Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server) => { - const mockResponse = (response) => [ +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const mockResponse = (response: Cluster[] | { itemsDeleted: string[]; errors: string[] }) => [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), ]; - const setLoadRemoteClustersResponse = (response) => { - server.respondWith('GET', '/api/remote_clusters', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); + const setLoadRemoteClustersResponse = (response: Cluster[] = []) => { + server.respondWith('GET', '/api/remote_clusters', mockResponse(response)); }; - const setDeleteRemoteClusterResponse = (response) => { + const setDeleteRemoteClusterResponse = ( + response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] } + ) => { server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response)); }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts similarity index 80% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts index 63084b21e3902..cf859ff6913f5 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts @@ -7,3 +7,4 @@ export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest'; export { setupEnvironment } from './setup_environment'; +export { createRemoteClustersActions, RemoteClustersActions } from './remote_clusters_actions'; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts new file mode 100644 index 0000000000000..ba0c424793838 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +export interface RemoteClustersActions { + docsButtonExists: () => boolean; + pageTitle: { + exists: () => boolean; + text: () => string; + }; + nameInput: { + setValue: (name: string) => void; + getValue: () => string; + isDisabled: () => boolean; + }; + skipUnavailableSwitch: { + exists: () => boolean; + toggle: () => void; + isChecked: () => boolean; + }; + connectionModeSwitch: { + exists: () => boolean; + toggle: () => void; + isChecked: () => boolean; + }; + cloudUrlSwitch: { + toggle: () => void; + exists: () => boolean; + isChecked: () => boolean; + }; + cloudUrlInput: { + exists: () => boolean; + getValue: () => string; + }; + seedsInput: { + setValue: (seed: string) => void; + getValue: () => string; + }; + proxyAddressInput: { + setValue: (proxyAddress: string) => void; + exists: () => boolean; + }; + serverNameInput: { + getLabel: () => string; + exists: () => boolean; + }; + saveButton: { + click: () => void; + isDisabled: () => boolean; + }; + getErrorMessages: () => string[]; + globalErrorExists: () => boolean; +} +export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersActions => { + const { form, exists, find, component } = testBed; + + const docsButtonExists = () => exists('remoteClusterDocsButton'); + const createPageTitleActions = () => { + const pageTitleSelector = 'remoteClusterPageTitle'; + return { + pageTitle: { + exists: () => exists(pageTitleSelector), + text: () => find(pageTitleSelector).text(), + }, + }; + }; + const createNameInputActions = () => { + const nameInputSelector = 'remoteClusterFormNameInput'; + return { + nameInput: { + setValue: (name: string) => form.setInputValue(nameInputSelector, name), + getValue: () => find(nameInputSelector).props().value, + isDisabled: () => find(nameInputSelector).props().disabled, + }, + }; + }; + + const createSkipUnavailableActions = () => { + const skipUnavailableToggleSelector = 'remoteClusterFormSkipUnavailableFormToggle'; + return { + skipUnavailableSwitch: { + exists: () => exists(skipUnavailableToggleSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(skipUnavailableToggleSelector); + }); + component.update(); + }, + isChecked: () => find(skipUnavailableToggleSelector).props()['aria-checked'], + }, + }; + }; + + const createConnectionModeActions = () => { + const connectionModeToggleSelector = 'remoteClusterFormConnectionModeToggle'; + return { + connectionModeSwitch: { + exists: () => exists(connectionModeToggleSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(connectionModeToggleSelector); + }); + component.update(); + }, + isChecked: () => find(connectionModeToggleSelector).props()['aria-checked'], + }, + }; + }; + + const createCloudUrlSwitchActions = () => { + const cloudUrlSelector = 'remoteClusterFormCloudUrlToggle'; + return { + cloudUrlSwitch: { + exists: () => exists(cloudUrlSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(cloudUrlSelector); + }); + component.update(); + }, + isChecked: () => find(cloudUrlSelector).props()['aria-checked'], + }, + }; + }; + + const createSeedsInputActions = () => { + const seedsInputSelector = 'remoteClusterFormSeedsInput'; + return { + seedsInput: { + setValue: (seed: string) => form.setComboBoxValue(seedsInputSelector, seed), + getValue: () => find(seedsInputSelector).text(), + }, + }; + }; + + const createProxyAddressActions = () => { + const proxyAddressSelector = 'remoteClusterFormProxyAddressInput'; + return { + proxyAddressInput: { + setValue: (proxyAddress: string) => form.setInputValue(proxyAddressSelector, proxyAddress), + exists: () => exists(proxyAddressSelector), + }, + }; + }; + + const createSaveButtonActions = () => { + const click = () => { + act(() => { + find('remoteClusterFormSaveButton').simulate('click'); + }); + + component.update(); + }; + const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled; + return { saveButton: { click, isDisabled } }; + }; + + const createServerNameActions = () => { + const serverNameSelector = 'remoteClusterFormServerNameFormRow'; + return { + serverNameInput: { + getLabel: () => find('remoteClusterFormServerNameFormRow').find('label').text(), + exists: () => exists(serverNameSelector), + }, + }; + }; + + const globalErrorExists = () => exists('remoteClusterFormGlobalError'); + + const createCloudUrlInputActions = () => { + const cloudUrlInputSelector = 'remoteClusterFormCloudUrlInput'; + return { + cloudUrlInput: { + exists: () => exists(cloudUrlInputSelector), + getValue: () => find(cloudUrlInputSelector).props().value, + }, + }; + }; + return { + docsButtonExists, + ...createPageTitleActions(), + ...createNameInputActions(), + ...createSkipUnavailableActions(), + ...createConnectionModeActions(), + ...createCloudUrlSwitchActions(), + ...createSeedsInputActions(), + ...createCloudUrlInputActions(), + ...createProxyAddressActions(), + ...createServerNameActions(), + ...createSaveButtonActions(), + getErrorMessages: form.getErrorsMessages, + globalErrorExists, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts similarity index 95% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts index 97ad344a63cc4..084552c5e6abe 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts @@ -36,6 +36,8 @@ export const setupEnvironment = () => { notificationServiceMock.createSetupContract().toasts, fatalErrorsServiceMock.createSetupContract() ); + // This expects HttpSetup but we're giving it AxiosInstance. + // @ts-ignore initHttp(mockHttpClient); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/remote_clusters/public/application/app_context.tsx b/x-pack/plugins/remote_clusters/public/application/app_context.tsx index 7931001c6faee..528ec322f49e1 100644 --- a/x-pack/plugins/remote_clusters/public/application/app_context.tsx +++ b/x-pack/plugins/remote_clusters/public/application/app_context.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { createContext } from 'react'; +import React, { createContext, useContext } from 'react'; export interface Context { isCloudEnabled: boolean; + cloudBaseUrl: string; } export const AppContext = createContext({} as any); @@ -22,3 +23,10 @@ export const AppContextProvider = ({ }) => { return {children}; }; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) throw new Error('Cannot use outside of app context'); + + return ctx; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/index.d.ts b/x-pack/plugins/remote_clusters/public/application/index.d.ts index 167297cedf556..45f981b5f2bc5 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.d.ts +++ b/x-pack/plugins/remote_clusters/public/application/index.d.ts @@ -12,7 +12,8 @@ export declare const renderApp: ( elem: HTMLElement | null, I18nContext: I18nStart['Context'], appDependencies: { - isCloudEnabled?: boolean; + isCloudEnabled: boolean; + cloudBaseUrl: string; }, history: ScopedHistory ) => ReturnType; 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 deleted file mode 100644 index 5f09193be90c2..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ /dev/null @@ -1,2017 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RemoteClusterForm proxy mode renders correct connection settings when user enables proxy mode 1`] = ` - - -
- - } - fullWidth={true} - title={ - -

- -

-
- } - > -
- -
- -
- - -

- - Name - -

-
-
- -
- -
- - A unique name for the cluster. - -
-
-
-
-
-
- -
- - } - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - helpText={ - - } - isInvalid={false} - label={ - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- - Name can only contain letters, numbers, underscores, and dashes. - -
-
-
-
-
-
-
-
-
-
-
- - - - - } - onChange={[Function]} - /> - - - } - fullWidth={true} - title={ - -

- -

-
- } - > -
- -
- -
- - -

- - Connection mode - -

-
-
- -
- -
- - Use seed nodes by default, or switch to proxy mode. - - -
-
- - } - onBlur={[Function]} - onChange={[Function]} - onFocus={[Function]} - > -
- - - - Use proxy mode - - -
-
-
-
-
-
-
-
-
-
-
- -
- - } - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - helpText={ - - } - isInvalid={false} - label={ - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- - The address to use for remote connections. - -
-
-
-
-
- - - , - } - } - /> - } - isInvalid={false} - label={ - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- - - , - } - } - > - A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. - - - - -
-
-
-
-
- - } - label={ - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- - The number of socket connections to open per remote cluster. - -
-
-
-
-
-
-
-
-
-
-
- -

- - - , - "optionName": - - , - } - } - /> -

- - } - fullWidth={true} - title={ - -

- -

-
- } - > -
- -
- -
- - -

- - Make remote cluster optional - -

-
-
- -
- -
-

- - - , - "optionName": - - , - } - } - > - A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable - - - Skip if unavailable - - - . - - - - -

-
-
-
-
-
-
- -
- -
-
- -
- - - Skip if unavailable - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- -
- -
- -
- - - - - -
-
-
-
-
-
- -
- - - -
-
-
-
- -`; - -exports[`RemoteClusterForm renders untouched state 1`] = ` -Array [ -
-
-
-
-

- Name -

-
-
- A unique name for the cluster. -
-
-
-
-
-
- -
-
-
-
- -
-
-
- Name can only contain letters, numbers, underscores, and dashes. -
-
-
-
-
-
-
-
-
-

- Connection mode -

-
-
- Use seed nodes by default, or switch to proxy mode. -
-
-
- - - Use proxy mode - -
-
-
-
-
-
-
-
-
- -
-
- -
-
-
- -
-
-
-
- -
-
-
- The number of gateway nodes to connect to for this cluster. -
-
-
-
-
-
-
-
-
-

- Make remote cluster optional -

-
-
-

- A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable - - Skip if unavailable - - . - -

-
-
-
-
-
-
-
- - - Skip if unavailable - -
-
-
-
-
-
-
, -
, -
-
-
-
- -
-
-
-
- -
-
, -] -`; - -exports[`RemoteClusterForm validation renders invalid state and a global form error when the user tries to submit an invalid form 1`] = ` -Array [ -
-
- -
-
-
-
- -
-
-
- Name is required. -
-
- Name can only contain letters, numbers, underscores, and dashes. -
-
-
, -
-
- -
-
- -
, -
-
-
- - - Skip if unavailable - -
-
-
, -
, -] -`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx new file mode 100644 index 0000000000000..1d4862ff094ce --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui'; +import { useAppContext } from '../../../../app_context'; + +export const CloudUrlHelp: FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + const { cloudBaseUrl } = useAppContext(); + return ( + + { + setIsOpen(!isOpen); + }} + > + + + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="upCenter" + > + + + + + + + + ), + elasticsearch: Elasticsearch, + }} + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx new file mode 100644 index 0000000000000..d06b4f111ec92 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiDescribedFormGroup, EuiTitle, EuiFormRow, EuiSwitch, EuiSpacer } from '@elastic/eui'; + +import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants'; +import { useAppContext } from '../../../../app_context'; + +import { ClusterErrors } from '../validators'; +import { SniffConnection } from './sniff_connection'; +import { ProxyConnection } from './proxy_connection'; +import { FormFields } from '../remote_cluster_form'; + +export interface Props { + fields: FormFields; + onFieldsChange: (fields: Partial) => void; + fieldsErrors: ClusterErrors; + areErrorsVisible: boolean; +} + +export const ConnectionMode: FunctionComponent = (props) => { + const { fields, onFieldsChange } = props; + const { mode, cloudUrlEnabled } = fields; + const { isCloudEnabled } = useAppContext(); + + return ( + +

+ +

+ + } + description={ + <> + {isCloudEnabled ? ( + <> + + + + } + checked={!cloudUrlEnabled} + data-test-subj="remoteClusterFormCloudUrlToggle" + onChange={(e) => onFieldsChange({ cloudUrlEnabled: !e.target.checked })} + /> + + + + ) : ( + <> + + + + } + checked={mode === PROXY_MODE} + data-test-subj="remoteClusterFormConnectionModeToggle" + onChange={(e) => + onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) + } + /> + + + )} + + } + fullWidth + > + {mode === SNIFF_MODE ? : } +
+ ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts new file mode 100644 index 0000000000000..864385ad0b1a3 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ConnectionMode } from './connection_mode'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx new file mode 100644 index 0000000000000..04e8533a0d2af --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldNumber, EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { useAppContext } from '../../../../app_context'; +import { proxySettingsUrl } from '../../../../services/documentation'; +import { Props } from './connection_mode'; +import { CloudUrlHelp } from './cloud_url_help'; + +export const ProxyConnection: FunctionComponent = (props) => { + const { fields, fieldsErrors, areErrorsVisible, onFieldsChange } = props; + const { isCloudEnabled } = useAppContext(); + const { proxyAddress, serverName, proxySocketConnections, cloudUrl, cloudUrlEnabled } = fields; + const { + proxyAddress: proxyAddressError, + serverName: serverNameError, + cloudUrl: cloudUrlError, + } = fieldsErrors; + + return ( + <> + {cloudUrlEnabled ? ( + <> + + } + labelAppend={} + isInvalid={Boolean(areErrorsVisible && cloudUrlError)} + error={cloudUrlError} + fullWidth + helpText={ + + } + > + onFieldsChange({ cloudUrl: e.target.value })} + isInvalid={Boolean(areErrorsVisible && cloudUrlError)} + data-test-subj="remoteClusterFormCloudUrlInput" + fullWidth + /> + + + ) : ( + <> + + } + helpText={ + + } + isInvalid={Boolean(areErrorsVisible && proxyAddressError)} + error={proxyAddressError} + fullWidth + > + onFieldsChange({ proxyAddress: e.target.value })} + isInvalid={Boolean(areErrorsVisible && proxyAddressError)} + data-test-subj="remoteClusterFormProxyAddressInput" + fullWidth + /> + + + + ) : ( + + ) + } + helpText={ + + + + ), + }} + /> + } + fullWidth + > + onFieldsChange({ serverName: e.target.value })} + isInvalid={Boolean(areErrorsVisible && serverNameError)} + fullWidth + /> + + + )} + + } + helpText={ + + } + fullWidth + > + onFieldsChange({ proxySocketConnections: Number(e.target.value) })} + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx new file mode 100644 index 0000000000000..063aeb3490aef --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFieldNumber, + EuiFormRow, + EuiLink, +} from '@elastic/eui'; + +import { transportPortUrl } from '../../../../services/documentation'; +import { validateSeed } from '../validators'; +import { Props } from './connection_mode'; + +export const SniffConnection: FunctionComponent = ({ + fields, + fieldsErrors, + areErrorsVisible, + onFieldsChange, +}) => { + const [localSeedErrors, setLocalSeedErrors] = useState([]); + const { seeds = [], nodeConnections } = fields; + const { seeds: seedsError } = fieldsErrors; + // Show errors if there is a general form error or local errors. + const areFormErrorsVisible = Boolean(areErrorsVisible && seedsError); + const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0; + const errors = + areFormErrorsVisible && seedsError ? localSeedErrors.concat(seedsError) : localSeedErrors; + const formattedSeeds: EuiComboBoxOptionOption[] = seeds.map((seed: string) => ({ label: seed })); + + const onCreateSeed = (newSeed?: string) => { + // If the user just hit enter without typing anything, treat it as a no-op. + if (!newSeed) { + return; + } + + const validationErrors = validateSeed(newSeed); + + if (validationErrors.length !== 0) { + setLocalSeedErrors(validationErrors); + // Return false to explicitly reject the user's input. + return false; + } + + const newSeeds = seeds.slice(0); + newSeeds.push(newSeed.toLowerCase()); + onFieldsChange({ seeds: newSeeds }); + }; + + const onSeedsInputChange = (seedInput?: string) => { + if (!seedInput) { + // If empty seedInput ("") don't do anything. This happens + // right after a seed is created. + return; + } + + // Allow typing to clear the errors, but not to add new ones. + const validationErrors = + !seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors; + + // EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the + // input is a duplicate. So we need to surface this error here instead. + const isDuplicate = seeds.includes(seedInput); + + if (isDuplicate) { + validationErrors.push( + + ); + } + + setLocalSeedErrors(validationErrors); + }; + return ( + <> + + } + helpText={ + + + + ), + }} + /> + } + isInvalid={showErrors} + error={errors} + fullWidth + > + + onFieldsChange({ seeds: options.map(({ label }) => label) }) + } + onSearchChange={onSeedsInputChange} + isInvalid={showErrors} + fullWidth + data-test-subj="remoteClusterFormSeedsInput" + /> + + + + } + helpText={ + + } + fullWidth + > + onFieldsChange({ nodeConnections: Number(e.target.value) })} + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.ts similarity index 100% rename from x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.js rename to x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.ts diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js deleted file mode 100644 index 325215d08af5f..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ /dev/null @@ -1,962 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { merge } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiComboBox, - EuiDescribedFormGroup, - EuiFieldNumber, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiLink, - EuiLoadingKibana, - EuiLoadingSpinner, - EuiOverlayMask, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, - EuiDelayRender, - EuiScreenReaderOnly, - htmlIdGenerator, -} from '@elastic/eui'; - -import { - skippingDisconnectedClustersUrl, - transportPortUrl, - proxySettingsUrl, -} from '../../../services/documentation'; - -import { RequestFlyout } from './request_flyout'; - -import { - validateName, - validateSeeds, - validateProxy, - validateSeed, - validateServerName, -} from './validators'; - -import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; - -import { AppContext } from '../../../app_context'; - -const defaultFields = { - name: '', - seeds: [], - skipUnavailable: false, - nodeConnections: 3, - proxyAddress: '', - proxySocketConnections: 18, - serverName: '', -}; - -const ERROR_TITLE_ID = 'removeClustersErrorTitle'; -const ERROR_LIST_ID = 'removeClustersErrorList'; - -export class RemoteClusterForm extends Component { - static propTypes = { - save: PropTypes.func.isRequired, - cancel: PropTypes.func, - isSaving: PropTypes.bool, - saveError: PropTypes.object, - fields: PropTypes.object, - disabledFields: PropTypes.object, - }; - - static defaultProps = { - fields: merge({}, defaultFields), - disabledFields: {}, - }; - - static contextType = AppContext; - - constructor(props, context) { - super(props, context); - - const { fields, disabledFields } = props; - const { isCloudEnabled } = context; - - // Connection mode should default to "proxy" in cloud - const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE; - const fieldsState = merge({}, { ...defaultFields, mode: defaultMode }, fields); - - this.generateId = htmlIdGenerator(); - this.state = { - localSeedErrors: [], - seedInput: '', - fields: fieldsState, - disabledFields, - fieldsErrors: this.getFieldsErrors(fieldsState), - areErrorsVisible: false, - isRequestVisible: false, - }; - } - - toggleRequest = () => { - this.setState(({ isRequestVisible }) => ({ - isRequestVisible: !isRequestVisible, - })); - }; - - getFieldsErrors(fields, seedInput = '') { - const { name, seeds, mode, proxyAddress, serverName } = fields; - const { isCloudEnabled } = this.context; - - return { - name: validateName(name), - seeds: mode === SNIFF_MODE ? validateSeeds(seeds, seedInput) : null, - proxyAddress: mode === PROXY_MODE ? validateProxy(proxyAddress) : null, - // server name is only required in cloud when proxy mode is enabled - serverName: isCloudEnabled && mode === PROXY_MODE ? validateServerName(serverName) : null, - }; - } - - onFieldsChange = (changedFields) => { - this.setState(({ fields: prevFields, seedInput }) => { - const newFields = { - ...prevFields, - ...changedFields, - }; - return { - fields: newFields, - fieldsErrors: this.getFieldsErrors(newFields, seedInput), - }; - }); - }; - - getAllFields() { - const { - fields: { - name, - mode, - seeds, - nodeConnections, - proxyAddress, - proxySocketConnections, - serverName, - skipUnavailable, - }, - } = this.state; - const { fields } = this.props; - - let modeSettings; - - if (mode === PROXY_MODE) { - modeSettings = { - proxyAddress, - proxySocketConnections, - serverName, - }; - } else { - modeSettings = { - seeds, - nodeConnections, - }; - } - - return { - name, - skipUnavailable, - mode, - hasDeprecatedProxySetting: fields.hasDeprecatedProxySetting, - ...modeSettings, - }; - } - - save = () => { - const { save } = this.props; - - if (this.hasErrors()) { - this.setState({ - areErrorsVisible: true, - }); - return; - } - - const cluster = this.getAllFields(); - save(cluster); - }; - - onCreateSeed = (newSeed) => { - // If the user just hit enter without typing anything, treat it as a no-op. - if (!newSeed) { - return; - } - - const localSeedErrors = validateSeed(newSeed); - - if (localSeedErrors.length !== 0) { - this.setState({ - localSeedErrors, - }); - - // Return false to explicitly reject the user's input. - return false; - } - - const { - fields: { seeds }, - } = this.state; - - const newSeeds = seeds.slice(0); - newSeeds.push(newSeed.toLowerCase()); - this.onFieldsChange({ seeds: newSeeds }); - }; - - onSeedsInputChange = (seedInput) => { - if (!seedInput) { - // If empty seedInput ("") don't do anything. This happens - // right after a seed is created. - return; - } - - this.setState(({ fields, localSeedErrors }) => { - const { seeds } = fields; - - // Allow typing to clear the errors, but not to add new ones. - const errors = !seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors; - - // EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the - // input is a duplicate. So we need to surface this error here instead. - const isDuplicate = seeds.includes(seedInput); - - if (isDuplicate) { - errors.push( - i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage', { - defaultMessage: `Duplicate seed nodes aren't allowed.`, - }) - ); - } - - return { - localSeedErrors: errors, - fieldsErrors: this.getFieldsErrors(fields, seedInput), - seedInput, - }; - }); - }; - - onSeedsChange = (seeds) => { - this.onFieldsChange({ seeds: seeds.map(({ label }) => label) }); - }; - - onSkipUnavailableChange = (e) => { - const skipUnavailable = e.target.checked; - this.onFieldsChange({ skipUnavailable }); - }; - - resetToDefault = (fieldName) => { - this.onFieldsChange({ - [fieldName]: defaultFields[fieldName], - }); - }; - - hasErrors = () => { - const { fieldsErrors, localSeedErrors } = this.state; - const errorValues = Object.values(fieldsErrors); - const hasErrors = errorValues.some((error) => error != null) || localSeedErrors.length; - return hasErrors; - }; - - renderSniffModeSettings() { - const { - areErrorsVisible, - fields: { seeds, nodeConnections }, - fieldsErrors: { seeds: errorsSeeds }, - localSeedErrors, - } = this.state; - - // Show errors if there is a general form error or local errors. - const areFormErrorsVisible = Boolean(areErrorsVisible && errorsSeeds); - const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0; - const errors = areFormErrorsVisible ? localSeedErrors.concat(errorsSeeds) : localSeedErrors; - - const formattedSeeds = seeds.map((seed) => ({ label: seed })); - - return ( - <> - - } - helpText={ - - - - ), - }} - /> - } - isInvalid={showErrors} - error={errors} - fullWidth - > - - - - - } - helpText={ - - } - fullWidth - > - - this.onFieldsChange({ nodeConnections: Number(e.target.value) || null }) - } - fullWidth - /> - - - ); - } - - renderProxyModeSettings() { - const { - areErrorsVisible, - fields: { proxyAddress, proxySocketConnections, serverName }, - fieldsErrors: { proxyAddress: errorProxyAddress, serverName: errorServerName }, - } = this.state; - - const { isCloudEnabled } = this.context; - - return ( - <> - - } - helpText={ - - } - isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} - error={errorProxyAddress} - fullWidth - > - this.onFieldsChange({ proxyAddress: e.target.value })} - isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} - data-test-subj="remoteClusterFormProxyAddressInput" - fullWidth - /> - - - - ) : ( - - ) - } - helpText={ - - - - ), - }} - /> - } - fullWidth - > - this.onFieldsChange({ serverName: e.target.value })} - isInvalid={Boolean(areErrorsVisible && errorServerName)} - fullWidth - /> - - - - } - helpText={ - - } - fullWidth - > - - this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) - } - fullWidth - /> - - - ); - } - - renderMode() { - const { - fields: { mode }, - } = this.state; - - const { isCloudEnabled } = this.context; - - return ( - -

- -

- - } - description={ - <> - - - - } - checked={mode === PROXY_MODE} - data-test-subj="remoteClusterFormConnectionModeToggle" - onChange={(e) => - this.onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) - } - /> - - {isCloudEnabled && mode === PROXY_MODE ? ( - <> - - - } - > - - - - ), - searchString: ( - - - - ), - }} - /> - - - ) : null} - - } - fullWidth - > - {mode === PROXY_MODE ? this.renderProxyModeSettings() : this.renderSniffModeSettings()} -
- ); - } - - renderSkipUnavailable() { - const { - fields: { skipUnavailable }, - } = this.state; - - return ( - -

- -

- - } - description={ - -

- - - - ), - learnMoreLink: ( - - - - ), - }} - /> -

-
- } - fullWidth - > - { - this.resetToDefault('skipUnavailable'); - }} - > - - - ) : null - } - > - - -
- ); - } - - renderActions() { - const { isSaving, cancel } = this.props; - const { areErrorsVisible, isRequestVisible } = this.state; - - if (isSaving) { - return ( - - - - - - - - - - - - ); - } - - let cancelButton; - - if (cancel) { - cancelButton = ( - - - - - - ); - } - - const isSaveDisabled = areErrorsVisible && this.hasErrors(); - - return ( - - - - - - - - - - {cancelButton} - - - - - - {isRequestVisible ? ( - - ) : ( - - )} - - - - ); - } - - renderSavingFeedback() { - if (this.props.isSaving) { - return ( - - - - ); - } - - return null; - } - - renderSaveErrorFeedback() { - const { saveError } = this.props; - - if (saveError) { - const { message, cause } = saveError; - - let errorBody; - - if (cause && Array.isArray(cause)) { - if (cause.length === 1) { - errorBody =

{cause[0]}

; - } else { - errorBody = ( -
    - {cause.map((causeValue) => ( -
  • {causeValue}
  • - ))} -
- ); - } - } - - return ( - - - {errorBody} - - - - - ); - } - - return null; - } - - renderErrors = () => { - const { - areErrorsVisible, - fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress }, - localSeedErrors, - } = this.state; - - const hasErrors = this.hasErrors(); - - if (!areErrorsVisible || !hasErrors) { - return null; - } - - const errorExplanations = []; - - if (errorClusterName) { - errorExplanations.push({ - key: 'nameExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', { - defaultMessage: 'The "Name" field is invalid.', - }), - error: errorClusterName, - }); - } - - if (errorsSeeds) { - errorExplanations.push({ - key: 'seedsExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', { - defaultMessage: 'The "Seed nodes" field is invalid.', - }), - error: errorsSeeds, - }); - } - - if (localSeedErrors && localSeedErrors.length) { - errorExplanations.push({ - key: 'localSeedExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage', { - defaultMessage: 'The "Seed nodes" field is invalid.', - }), - error: localSeedErrors.join(' '), - }); - } - - if (errorProxyAddress) { - errorExplanations.push({ - key: 'seedsExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { - defaultMessage: 'The "Proxy address" field is invalid.', - }), - error: errorProxyAddress, - }); - } - - const messagesToBeRendered = errorExplanations.length && ( - -
- {errorExplanations.map(({ key, field, error }) => ( -
-
{field}
-
{error}
-
- ))} -
-
- ); - - return ( - - - - -

- } - color="danger" - iconType="cross" - /> - {messagesToBeRendered} - - ); - }; - - render() { - const { - disabledFields: { name: disabledName }, - } = this.props; - - const { - isRequestVisible, - areErrorsVisible, - fields: { name }, - fieldsErrors: { name: errorClusterName }, - } = this.state; - - return ( - - {this.renderSaveErrorFeedback()} - - - -

- -

-
- } - description={ - - } - fullWidth - > - - } - helpText={ - - } - error={errorClusterName} - isInvalid={Boolean(areErrorsVisible && errorClusterName)} - fullWidth - > - this.onFieldsChange({ name: e.target.value })} - fullWidth - disabled={disabledName} - data-test-subj="remoteClusterFormNameInput" - /> - - - - {this.renderMode()} - - {this.renderSkipUnavailable()} - - - {this.renderErrors()} - - - - {this.renderActions()} - - {this.renderSavingFeedback()} - - {isRequestVisible ? ( - this.setState({ isRequestVisible: false })} - /> - ) : null} - - ); - } -} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js deleted file mode 100644 index 2ae16b8ca7cbf..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mountWithIntl, renderWithIntl } from '@kbn/test/jest'; -import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { RemoteClusterForm } from './remote_cluster_form'; - -// Make sure we have deterministic aria IDs. -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: (prefix = 'staticGenerator') => (suffix = 'staticId') => `${prefix}_${suffix}`, -})); - -describe('RemoteClusterForm', () => { - test(`renders untouched state`, () => { - const component = renderWithIntl( {}} />); - expect(component).toMatchSnapshot(); - }); - - describe('proxy mode', () => { - test('renders correct connection settings when user enables proxy mode', () => { - const component = mountWithIntl( {}} />); - - findTestSubject(component, 'remoteClusterFormConnectionModeToggle').simulate('click'); - - expect(component).toMatchSnapshot(); - }); - }); - - describe('validation', () => { - test('renders invalid state and a global form error when the user tries to submit an invalid form', () => { - const component = mountWithIntl( {}} />); - - findTestSubject(component, 'remoteClusterFormSaveButton').simulate('click'); - - const fieldsSnapshot = [ - 'remoteClusterFormNameFormRow', - 'remoteClusterFormSeedNodesFormRow', - 'remoteClusterFormSkipUnavailableFormRow', - 'remoteClusterFormGlobalError', - ].map((testSubject) => { - const mountedField = findTestSubject(component, testSubject); - return takeMountedSnapshot(mountedField); - }); - - expect(fieldsSnapshot).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx new file mode 100644 index 0000000000000..9f6eee757c755 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx @@ -0,0 +1,629 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component, Fragment } from 'react'; +import { merge } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiLoadingKibana, + EuiLoadingSpinner, + EuiOverlayMask, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, + EuiDelayRender, + EuiScreenReaderOnly, + htmlIdGenerator, + EuiSwitchEvent, +} from '@elastic/eui'; + +import { Cluster } from '../../../../../common/lib'; +import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; + +import { AppContext, Context } from '../../../app_context'; + +import { skippingDisconnectedClustersUrl } from '../../../services/documentation'; + +import { RequestFlyout } from './request_flyout'; +import { ConnectionMode } from './components'; +import { + ClusterErrors, + convertCloudUrlToProxyConnection, + convertProxyConnectionToCloudUrl, + validateCluster, +} from './validators'; +import { isCloudUrlEnabled } from './validators/validate_cloud_url'; + +const defaultClusterValues: Cluster = { + name: '', + seeds: [], + skipUnavailable: false, + nodeConnections: 3, + proxyAddress: '', + proxySocketConnections: 18, + serverName: '', +}; + +const ERROR_TITLE_ID = 'removeClustersErrorTitle'; +const ERROR_LIST_ID = 'removeClustersErrorList'; + +interface Props { + save: (cluster: Cluster) => void; + cancel?: () => void; + isSaving?: boolean; + saveError?: any; + cluster?: Cluster; +} + +export type FormFields = Cluster & { cloudUrl: string; cloudUrlEnabled: boolean }; + +interface State { + fields: FormFields; + fieldsErrors: ClusterErrors; + areErrorsVisible: boolean; + isRequestVisible: boolean; +} + +export class RemoteClusterForm extends Component { + static defaultProps = { + fields: merge({}, defaultClusterValues), + }; + + static contextType = AppContext; + private readonly generateId: (idSuffix?: string) => string; + + constructor(props: Props, context: Context) { + super(props, context); + + const { cluster } = props; + const { isCloudEnabled } = context; + + // Connection mode should default to "proxy" in cloud + const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE; + const fieldsState: FormFields = merge( + {}, + { + ...defaultClusterValues, + mode: defaultMode, + cloudUrl: convertProxyConnectionToCloudUrl(cluster), + cloudUrlEnabled: isCloudEnabled && isCloudUrlEnabled(cluster), + }, + cluster + ); + + this.generateId = htmlIdGenerator(); + this.state = { + fields: fieldsState, + fieldsErrors: validateCluster(fieldsState, isCloudEnabled), + areErrorsVisible: false, + isRequestVisible: false, + }; + } + + toggleRequest = () => { + this.setState(({ isRequestVisible }) => ({ + isRequestVisible: !isRequestVisible, + })); + }; + + onFieldsChange = (changedFields: Partial) => { + const { isCloudEnabled } = this.context; + + // when cloudUrl changes, fill proxy address and server name + const { cloudUrl } = changedFields; + if (cloudUrl) { + const { proxyAddress, serverName } = convertCloudUrlToProxyConnection(cloudUrl); + changedFields = { + ...changedFields, + proxyAddress, + serverName, + }; + } + + this.setState(({ fields: prevFields }) => { + const newFields = { + ...prevFields, + ...changedFields, + }; + return { + fields: newFields, + fieldsErrors: validateCluster(newFields, isCloudEnabled), + }; + }); + }; + + getCluster(): Cluster { + const { + fields: { + name, + mode, + seeds, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + skipUnavailable, + }, + } = this.state; + const { cluster } = this.props; + + let modeSettings; + + if (mode === PROXY_MODE) { + modeSettings = { + proxyAddress, + proxySocketConnections, + serverName, + }; + } else { + modeSettings = { + seeds, + nodeConnections, + }; + } + + return { + name, + skipUnavailable, + mode, + hasDeprecatedProxySetting: cluster?.hasDeprecatedProxySetting, + ...modeSettings, + }; + } + + save = () => { + const { save } = this.props; + + if (this.hasErrors()) { + this.setState({ + areErrorsVisible: true, + }); + return; + } + + const cluster = this.getCluster(); + save(cluster); + }; + + onSkipUnavailableChange = (e: EuiSwitchEvent) => { + const skipUnavailable = e.target.checked; + this.onFieldsChange({ skipUnavailable }); + }; + + resetToDefault = (fieldName: keyof Cluster) => { + this.onFieldsChange({ + [fieldName]: defaultClusterValues[fieldName], + }); + }; + + hasErrors = () => { + const { fieldsErrors } = this.state; + const errorValues = Object.values(fieldsErrors); + return errorValues.some((error) => error != null); + }; + + renderSkipUnavailable() { + const { + fields: { skipUnavailable }, + } = this.state; + + return ( + +

+ +

+ + } + description={ + +

+ + + + ), + learnMoreLink: ( + + + + ), + }} + /> +

+
+ } + fullWidth + > + { + this.resetToDefault('skipUnavailable'); + }} + > + + + ) : null + } + > + + +
+ ); + } + + renderActions() { + const { isSaving, cancel } = this.props; + const { areErrorsVisible, isRequestVisible } = this.state; + + if (isSaving) { + return ( + + + + + + + + + + + + ); + } + + let cancelButton; + + if (cancel) { + cancelButton = ( + + + + + + ); + } + + const isSaveDisabled = areErrorsVisible && this.hasErrors(); + + return ( + + + + + + + + + + {cancelButton} + + + + + + {isRequestVisible ? ( + + ) : ( + + )} + + + + ); + } + + renderSavingFeedback() { + if (this.props.isSaving) { + return ( + + + + ); + } + + return null; + } + + renderSaveErrorFeedback() { + const { saveError } = this.props; + + if (saveError) { + const { message, cause } = saveError; + + let errorBody; + + if (cause && Array.isArray(cause)) { + if (cause.length === 1) { + errorBody =

{cause[0]}

; + } else { + errorBody = ( +
    + {cause.map((causeValue) => ( +
  • {causeValue}
  • + ))} +
+ ); + } + } + + return ( + + + {errorBody} + + + + + ); + } + + return null; + } + + renderErrors = () => { + const { + areErrorsVisible, + fieldsErrors: { + name: errorClusterName, + seeds: errorsSeeds, + proxyAddress: errorProxyAddress, + serverName: errorServerName, + cloudUrl: errorCloudUrl, + }, + } = this.state; + + const hasErrors = this.hasErrors(); + + if (!areErrorsVisible || !hasErrors) { + return null; + } + + const errorExplanations = []; + + if (errorClusterName) { + errorExplanations.push({ + key: 'nameExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', { + defaultMessage: 'The "Name" field is invalid.', + }), + error: errorClusterName, + }); + } + + if (errorsSeeds) { + errorExplanations.push({ + key: 'seedsExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', { + defaultMessage: 'The "Seed nodes" field is invalid.', + }), + error: errorsSeeds, + }); + } + + if (errorProxyAddress) { + errorExplanations.push({ + key: 'proxyAddressExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { + defaultMessage: 'The "Proxy address" field is invalid.', + }), + error: errorProxyAddress, + }); + } + + if (errorServerName) { + errorExplanations.push({ + key: 'serverNameExplanation', + field: i18n.translate( + 'xpack.remoteClusters.remoteClusterForm.inputServerNameErrorMessage', + { + defaultMessage: 'The "Server name" field is invalid.', + } + ), + error: errorServerName, + }); + } + + if (errorCloudUrl) { + errorExplanations.push({ + key: 'cloudUrlExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputcloudUrlErrorMessage', { + defaultMessage: 'The "Elasticsearch endpoint URL" field is invalid.', + }), + error: errorCloudUrl, + }); + } + + const messagesToBeRendered = errorExplanations.length && ( + +
+ {errorExplanations.map(({ key, field, error }) => ( +
+
{field}
+
{error}
+
+ ))} +
+
+ ); + + return ( + + + + + + } + color="danger" + iconType="cross" + /> + {messagesToBeRendered} + + ); + }; + + render() { + const { isRequestVisible, areErrorsVisible, fields, fieldsErrors } = this.state; + const { name: errorClusterName } = fieldsErrors; + const { cluster } = this.props; + const isNew = !cluster; + return ( + + {this.renderSaveErrorFeedback()} + + + +

+ +

+ + } + description={ + + } + fullWidth + > + + } + helpText={ + + } + error={errorClusterName} + isInvalid={Boolean(areErrorsVisible && errorClusterName)} + fullWidth + > + this.onFieldsChange({ name: e.target.value })} + fullWidth + disabled={!isNew} + data-test-subj="remoteClusterFormNameInput" + /> + +
+ + + + {this.renderSkipUnavailable()} +
+ + {this.renderErrors()} + + + + {this.renderActions()} + + {this.renderSavingFeedback()} + + {isRequestVisible ? ( + this.setState({ isRequestVisible: false })} + /> + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx index 4e402b8b55a5b..2bcedc2bce458 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx @@ -24,13 +24,13 @@ import { Cluster, serializeCluster } from '../../../../../common/lib'; interface Props { close: () => void; - name: string; cluster: Cluster; } export class RequestFlyout extends PureComponent { render() { - const { name, close, cluster } = this.props; + const { close, cluster } = this.props; + const { name } = cluster; const endpoint = 'PUT _cluster/settings'; const payload = JSON.stringify(serializeCluster(cluster), null, 2); const request = `${endpoint}\n${payload}`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts index 67a5d8f727f3e..6f3956a19f6a0 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts @@ -10,3 +10,10 @@ export { validateProxy } from './validate_proxy'; export { validateSeeds } from './validate_seeds'; export { validateSeed } from './validate_seed'; export { validateServerName } from './validate_server_name'; +export { validateCluster, ClusterErrors } from './validate_cluster'; +export { + isCloudUrlEnabled, + validateCloudUrl, + convertProxyConnectionToCloudUrl, + convertCloudUrlToProxyConnection, +} from './validate_cloud_url'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts new file mode 100644 index 0000000000000..599706ba85b02 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + isCloudUrlEnabled, + validateCloudUrl, + convertCloudUrlToProxyConnection, + convertProxyConnectionToCloudUrl, + i18nTexts, +} from './validate_cloud_url'; + +describe('Cloud url', () => { + describe('validation', () => { + it('errors when the url is empty', () => { + const actual = validateCloudUrl(''); + expect(actual).toBe(i18nTexts.urlEmpty); + }); + + it('errors when the url is invalid', () => { + const actual = validateCloudUrl('invalid%url'); + expect(actual).toBe(i18nTexts.urlInvalid); + }); + }); + + describe('is cloud url', () => { + it('true for a new cluster', () => { + const actual = isCloudUrlEnabled(); + expect(actual).toBe(true); + }); + + it('true when proxy connection is empty', () => { + const actual = isCloudUrlEnabled({ name: 'test', proxyAddress: '', serverName: '' }); + expect(actual).toBe(true); + }); + + it('true when proxy address is the same as server name and default port', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:9400', + serverName: 'some-proxy', + }); + expect(actual).toBe(true); + }); + it('false when proxy address is the same as server name but not default port', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:1234', + serverName: 'some-proxy', + }); + expect(actual).toBe(false); + }); + it('true when proxy address is not the same as server name', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:9400', + serverName: 'some-server-name', + }); + expect(actual).toBe(false); + }); + }); + describe('conversion from cloud url', () => { + it('empty url to empty proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection(''); + expect(actual).toEqual({ proxyAddress: '', serverName: '' }); + }); + + it('url with protocol and port to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('http://test.com:1234'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + + it('url with protocol and no port to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('http://test.com'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + + it('url with no protocol to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('test.com'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + it('invalid url to empty proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('invalid%url'); + expect(actual).toEqual({ proxyAddress: '', serverName: '' }); + }); + }); + + describe('conversion to cloud url', () => { + it('empty proxy address to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: '', + serverName: 'test', + }); + expect(actual).toEqual(''); + }); + + it('empty server name to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test', + serverName: '', + }); + expect(actual).toEqual(''); + }); + + it('different proxy address and server name to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test', + serverName: 'another-test', + }); + expect(actual).toEqual(''); + }); + + it('valid proxy connection to cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test-proxy:9400', + serverName: 'test-proxy', + }); + expect(actual).toEqual('test-proxy'); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx new file mode 100644 index 0000000000000..1f4862f0113e7 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Cluster } from '../../../../../../common/lib'; +import { isAddressValid } from './validate_address'; + +export const i18nTexts = { + urlEmpty: ( + + ), + urlInvalid: ( + + ), +}; + +const CLOUD_DEFAULT_PROXY_PORT = '9400'; +const EMPTY_PROXY_VALUES = { proxyAddress: '', serverName: '' }; +const PROTOCOL_REGEX = new RegExp(/^https?:\/\//); + +export const isCloudUrlEnabled = (cluster?: Cluster): boolean => { + // enable cloud url for new clusters + if (!cluster) { + return true; + } + const { proxyAddress, serverName } = cluster; + if (!proxyAddress && !serverName) { + return true; + } + const portParts = (proxyAddress ?? '').split(':'); + const proxyAddressWithoutPort = portParts[0]; + const port = portParts[1]; + return port === CLOUD_DEFAULT_PROXY_PORT && proxyAddressWithoutPort === serverName; +}; + +const formatUrl = (url: string) => { + url = (url ?? '').trim().toLowerCase(); + // delete http(s):// protocol string if any + url = url.replace(PROTOCOL_REGEX, ''); + return url; +}; + +export const convertProxyConnectionToCloudUrl = (cluster?: Cluster): string => { + if (!isCloudUrlEnabled(cluster)) { + return ''; + } + return cluster?.serverName ?? ''; +}; +export const convertCloudUrlToProxyConnection = ( + cloudUrl: string = '' +): { proxyAddress: string; serverName: string } => { + cloudUrl = formatUrl(cloudUrl); + if (!cloudUrl || !isAddressValid(cloudUrl)) { + return EMPTY_PROXY_VALUES; + } + const address = cloudUrl.split(':')[0]; + return { proxyAddress: `${address}:${CLOUD_DEFAULT_PROXY_PORT}`, serverName: address }; +}; + +export const validateCloudUrl = (cloudUrl: string): JSX.Element | null => { + if (!cloudUrl) { + return i18nTexts.urlEmpty; + } + cloudUrl = formatUrl(cloudUrl); + if (!isAddressValid(cloudUrl)) { + return i18nTexts.urlInvalid; + } + return null; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx new file mode 100644 index 0000000000000..e0fa434f21d5c --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateName } from './validate_name'; +import { PROXY_MODE, SNIFF_MODE } from '../../../../../../common/constants'; +import { validateSeeds } from './validate_seeds'; +import { validateProxy } from './validate_proxy'; +import { validateServerName } from './validate_server_name'; +import { validateCloudUrl } from './validate_cloud_url'; +import { FormFields } from '../remote_cluster_form'; + +type ClusterError = JSX.Element | null; + +export interface ClusterErrors { + name?: ClusterError; + seeds?: ClusterError; + proxyAddress?: ClusterError; + serverName?: ClusterError; + cloudUrl?: ClusterError; +} +export const validateCluster = (fields: FormFields, isCloudEnabled: boolean): ClusterErrors => { + const { name, seeds = [], mode, proxyAddress, serverName, cloudUrlEnabled, cloudUrl } = fields; + + return { + name: validateName(name), + seeds: mode === SNIFF_MODE ? validateSeeds(seeds) : null, + proxyAddress: !cloudUrlEnabled && mode === PROXY_MODE ? validateProxy(proxyAddress) : null, + // server name is only required in cloud when proxy mode is enabled + serverName: + !cloudUrlEnabled && isCloudEnabled && mode === PROXY_MODE + ? validateServerName(serverName) + : null, + cloudUrl: cloudUrlEnabled ? validateCloudUrl(cloudUrl) : null, + }; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts deleted file mode 100644 index a5b3656b36de5..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -import { isAddressValid, isPortValid } from './validate_address'; - -export function validateSeed(seed?: string): string[] { - const errors: string[] = []; - - if (!seed) { - return errors; - } - - const isValid = isAddressValid(seed); - - if (!isValid) { - errors.push( - i18n.translate( - 'xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage', - { - defaultMessage: - 'Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. ' + - 'Hosts can only consist of letters, numbers, and dashes.', - } - ) - ); - } - - if (!isPortValid(seed)) { - errors.push( - i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage', { - defaultMessage: 'A port is required.', - }) - ); - } - - return errors; -} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx new file mode 100644 index 0000000000000..4863dff5ec337 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { isAddressValid, isPortValid } from './validate_address'; + +export function validateSeed(seed?: string): JSX.Element[] { + const errors: JSX.Element[] = []; + + if (!seed) { + return errors; + } + + const isValid = isAddressValid(seed); + + if (!isValid) { + errors.push( + + ); + } + + if (!isPortValid(seed)) { + errors.push( + + ); + } + + return errors; +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/index.d.ts b/x-pack/plugins/remote_clusters/public/application/sections/index.d.ts new file mode 100644 index 0000000000000..ab0f579c1a415 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/index.d.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ComponentType } from 'react'; + +export declare const RemoteClusterEdit: ComponentType; +export declare const RemoteClusterAdd: ComponentType; +export declare const RemoteClusterList: ComponentType; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 6ee6bd6d87d58..124d2d42afb78 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -73,7 +73,7 @@ export class RemoteClusterAdd extends PureComponent { description={ } /> diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index c68dd7ab10aa7..18ee2e2b3875d 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -27,10 +27,6 @@ import { getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components'; -const disabledFields = { - name: true, -}; - export class RemoteClusterEdit extends Component { static propTypes = { isLoading: PropTypes.bool, @@ -202,8 +198,7 @@ export class RemoteClusterEdit extends Component { ) : null} Store; diff --git a/x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png b/x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c9302ef76eac6184fad0eb6931558c94980e54 GIT binary patch literal 197089 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 z z)6xu)kFX`3u3CPjPSx34pfOqRZ2chZoU`jFJ!eWIFs{sS z-|p4HHBmJ$+9In!Yraq|#f@Xy8BR#9j{=&FiiJs-+Aemvi!Lk4&AKn;U1VFrsb7?sO@AAPrh) za#nBpaiGb`NyB7eS!889T#HLJi5HV8+7?aLLQ6z#b+LlzopiiLpe6sMnn8!6&X;0` zdvv>?Lj~Bf_!NtC)<>)K3Asm)aYkwsdrpauCY)z}_WaCVz+fo)~$adQ+ z5GR|1M%#73NIQWLRh@!aOStH$3KsFuQU6jd4&OUjNOlsBHDti~0lgsQrP0G+3Rlqf ztaCAEo4Dvpm+BZ6G>J*W?Wilxo^ax`DlCNeQkyq<6EgJZ8G%w1YouZ%3!sHS$o`U? zmO}N~Hb9$f7CWPzZ+XB}F;&t$tQh_U{n=Zw4DdZ%DWf*WZEy1RUVEmn&2)Weg%9*_ zF>m=TZqr+&$%zPWvNx)7d{D13L+>{oLJ-5^m^BzmwFa}8ZMGO=(Y1Tb{8XWNOQO)6 zTwUTjIq<#Nma+fBJ|cGqEH0DL zS?;GB{V4}QQGbarqjrD7b_XZRMx(eK2gt4TWu8u_(>iuiO`kKV8oOQYZh#rn{m((o zut|p8miGuI?eY+kh1KP3jOsQ%p8}HdyuK~NMp6ycZU8bUB_}md+F2L2Ok0sR`^0S^ ztf|kR4(|%6y~VCe7jWC`1f(qPX4k8F1fHv>&TUi{=)RA zofSraSC$vMO^0l0+{l$X2<-_jmC#m$mPNS0R3*_TnPFk|vfjw?qcNlL9@dU7-yk<5 z|NB(7wt4!iQ!1mY+RP%5f{ovDMT?ie^UUh(hC`eV#OcZoRgi5jQ}gVF8tBzzgO)(;YypRr!V<)1zP1ntgiE=6-pUD)){k3#?X%Xd6Xxsgl z8n<2U`P^mo2P*@^a+ba2kH}6k4>yq!Ieav?9PJ
;gLBW7KrTwnn@iuets>=U!`- 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

M|4!~-3cgu7U#8Pr4HL9(7d^4r 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 Kq*(UloBsm`DHQVn literal 0 HcmV?d00001 diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index 107d4e127d1b5..540a8b40a6208 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -60,13 +60,14 @@ export class RemoteClustersUIPlugin initNotification(toasts, fatalErrors); initHttp(http); - const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled); + const cloudBaseUrl: string = cloud?.baseUrl ?? ''; const { renderApp } = await import('./application'); const unmountAppCallback = await renderApp( element, i18nContext, - { isCloudEnabled }, + { isCloudEnabled, cloudBaseUrl }, history ); diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json index 0bee6300cf0b2..9dc7926bd62ea 100644 --- a/x-pack/plugins/remote_clusters/tsconfig.json +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -8,10 +8,12 @@ "declarationMap": true }, "include": [ + "__jest__/**/*", "common/**/*", "fixtures/**/*", "public/**/*", "server/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0c16860acf56c..3f9be18e33d2a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16761,10 +16761,6 @@ "xpack.remoteClusters.addTitle": "リモートクラスターを追加", "xpack.remoteClusters.appName": "リモートクラスター", "xpack.remoteClusters.appTitle": "リモートクラスター", - "xpack.remoteClusters.cloudClusterInformationDescription": "クラスターのプロキシアドレスとサーバー名を見つけるには、デプロイメニューの{security}ページに移動し、{searchString}を検索します。", - "xpack.remoteClusters.cloudClusterInformationTitle": "Elasticsearch Cloudデプロイのプロキシモードを使用", - "xpack.remoteClusters.cloudClusterSearchDescription": "リモートクラスターパラメーター", - "xpack.remoteClusters.cloudClusterSecurityDescription": "セキュリティ", "xpack.remoteClusters.configuredByNodeWarningTitle": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。", "xpack.remoteClusters.connectedStatus.connectedAriaLabel": "接続済み", "xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続", @@ -16838,7 +16834,6 @@ "xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "サーバー名", "xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "リモートクラスターごとに開くソケット接続の数。", "xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "リクエストを非表示", - "xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "「シードノード」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "「名前」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "「プロキシアドレス」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "「シードノード」フィールドが無効です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5e5f53356a2e8..25aa56d031fca 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16987,10 +16987,6 @@ "xpack.remoteClusters.addTitle": "添加远程集群", "xpack.remoteClusters.appName": "远程集群", "xpack.remoteClusters.appTitle": "远程集群", - "xpack.remoteClusters.cloudClusterInformationDescription": "要查找您的集群的代理地址和服务器名称,请前往部署菜单的{security}页面并搜索“{searchString}”。", - "xpack.remoteClusters.cloudClusterInformationTitle": "将代理模式用于 Elastic Cloud 部署", - "xpack.remoteClusters.cloudClusterSearchDescription": "远程集群参数", - "xpack.remoteClusters.cloudClusterSecurityDescription": "安全", "xpack.remoteClusters.configuredByNodeWarningTitle": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。", "xpack.remoteClusters.connectedStatus.connectedAriaLabel": "已连接", "xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接", @@ -17065,7 +17061,6 @@ "xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "服务器名", "xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "每个远程集群要打开的套接字数目。", "xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "隐藏请求", - "xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "“种子节点”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "“名称”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "“代理地址”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "“种子节点”字段无效。", From 75e4aeedbc46bdc5c4e0df859aa5a93a13e1e0b3 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 8 Apr 2021 14:16:31 +0100 Subject: [PATCH 109/131] [ML] Excludes metadata fields from jobs caps fields service response (#96548) --- .../ml/server/models/job_service/new_job_caps/field_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 0287c2af11a7e..c6cf608fe1e0b 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -80,7 +80,7 @@ class FieldsService { if (firstKey !== undefined) { const field = fc[firstKey]; // add to the list of fields if the field type can be used by ML - if (supportedTypes.includes(field.type) === true) { + if (supportedTypes.includes(field.type) === true && field.metadata_field !== true) { fields.push({ id: k, name: k, From 02ba7c4543a265fc897c2354a323b22e30caf192 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 8 Apr 2021 08:49:13 -0500 Subject: [PATCH 110/131] [kbn-ui-shared-deps] Remove outdated polyfills (#96339) --- NOTICE.txt | 27 ----------- package.json | 1 - packages/kbn-ui-shared-deps/polyfills.js | 3 -- .../vendor/childnode_remove_polyfill.js | 48 ------------------- yarn.lock | 5 -- 5 files changed, 84 deletions(-) delete mode 100644 packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js diff --git a/NOTICE.txt b/NOTICE.txt index 2341a478cbda9..4eec329b7a603 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -261,33 +261,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -This product bundles childnode-remove which is available under a -"MIT" license. - -The MIT License (MIT) - -Copyright (c) 2016-present, jszhou -https://github.com/jserz/js_piece - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - --- This product bundles code based on probot-metadata@1.0.0 which is available under a "MIT" license. diff --git a/package.json b/package.json index ffe1a10f0bfea..9bddca4665467 100644 --- a/package.json +++ b/package.json @@ -208,7 +208,6 @@ "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", - "custom-event-polyfill": "^0.3.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js index abbf911cfc8fc..a9ec32023f2bf 100644 --- a/packages/kbn-ui-shared-deps/polyfills.js +++ b/packages/kbn-ui-shared-deps/polyfills.js @@ -8,7 +8,6 @@ require('core-js/stable'); require('regenerator-runtime/runtime'); -require('custom-event-polyfill'); if (typeof window.Event === 'object') { // IE11 doesn't support unknown event types, required by react-use @@ -17,6 +16,4 @@ if (typeof window.Event === 'object') { } require('whatwg-fetch'); -require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); -require('./vendor/childnode_remove_polyfill'); require('symbol-observable'); diff --git a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js deleted file mode 100644 index d8818fe809ccb..0000000000000 --- a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -/* @notice - * This product bundles childnode-remove which is available under a - * "MIT" license. - * - * The MIT License (MIT) - * - * Copyright (c) 2016-present, jszhou - * https://github.com/jserz/js_piece - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* eslint-disable */ - -(function (arr) { - arr.forEach(function (item) { - if (item.hasOwnProperty('remove')) { - return; - } - Object.defineProperty(item, 'remove', { - configurable: true, - enumerable: true, - writable: true, - value: function remove() { - if (this.parentNode !== null) - this.parentNode.removeChild(this); - } - }); - }); -})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); diff --git a/yarn.lock b/yarn.lock index 786143fb3d1ef..0e6427d2e265e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10950,11 +10950,6 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -custom-event-polyfill@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888" - integrity sha1-mYB4Ob5i7bRGtkWDLg2A6tb6GIg= - cwise-compiler@^1.0.0, cwise-compiler@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5" From 5d54e2990b27a6febf1e38d909ad066d379c1631 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 8 Apr 2021 15:50:23 +0200 Subject: [PATCH 111/131] Update developer docs for upgrading Node.js (#96422) --- docs/developer/advanced/upgrading-nodejs.asciidoc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc index c1e727b1eac65..3827cb6e9aa7d 100644 --- a/docs/developer/advanced/upgrading-nodejs.asciidoc +++ b/docs/developer/advanced/upgrading-nodejs.asciidoc @@ -14,10 +14,14 @@ Theses files must be updated when upgrading Node.js: - {kib-repo}blob/{branch}/.node-version[`.node-version`] - {kib-repo}blob/{branch}/.nvmrc[`.nvmrc`] - {kib-repo}blob/{branch}/package.json[`package.json`] - The version is specified in the `engines.node` field. + - {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property. + Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes. + These can be found on the https://nodejs.org[nodejs.org] website. + Example for Node.js v14.16.1: https://nodejs.org/dist/v14.16.1/SHASUMS256.txt.asc -See PR {kib-repo}pull/86593[#86593] for an example of how the Node.js version has been upgraded previously. +See PR {kib-repo}pull/96382[#96382] for an example of how the Node.js version has been upgraded previously. -In the 6.8 branch, the `.ci/Dockerfile` file does not exist, so when upgrading Node.js in that branch, just skip that file. +In the 6.8 branch, neither the `.ci/Dockerfile` file nor the `WORKSPACE.bazel` file exists, so when upgrading Node.js in that branch, just skip those files. === Backporting From e3f31ea9ad6ef84de4519e5799776b247c45ef16 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Thu, 8 Apr 2021 17:29:57 +0300 Subject: [PATCH 112/131] [Visualize] Allows editing broken visualizations caused by runtime fields changes (#94798) * Add possibility to open visualization when saved field doesn't exists anymore * Fix tests * Fix some remarks * Remove unneeded code * Fix tests * Fix tests * Fix some remarks * Fixed problem with double error popover in visualizations * Fix CI * Fix type * Fix API docs * Don't show error popup for error related to runtime fields * Fix some remarks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...gin-plugins-data-public.kbn_field_types.md | 1 + ...gin-plugins-data-server.kbn_field_types.md | 1 + ...mbeddable-public.embeddable.getupdated_.md | 4 +- .../data/common/kbn_field_types/types.ts | 1 + .../common/search/aggs/agg_configs.test.ts | 2 +- .../_terms_other_bucket_helper.test.ts | 21 ++++- .../common/search/aggs/buckets/terms.test.ts | 78 ++++++++++++++----- .../common/search/aggs/param_types/field.ts | 76 ++++++++++++------ src/plugins/data/public/public.api.md | 2 + src/plugins/data/server/server.api.md | 2 + .../public/lib/embeddables/embeddable.tsx | 7 +- src/plugins/embeddable/public/public.api.md | 2 +- .../kibana_utils/common/errors/errors.ts | 22 +++++- .../public/components/controls/field.test.tsx | 15 +++- .../public/components/controls/field.tsx | 46 ++++++++++- .../public/embeddable/visualize_embeddable.ts | 3 +- .../utils/get_visualization_instance.ts | 11 ++- .../utils/use/use_saved_vis_instance.ts | 26 +------ .../public/application/utils/utils.ts | 35 +++++++++ 19 files changed, 268 insertions(+), 87 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md index 4d75dda61d5c9..521ceeb1e37f2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md @@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES | HISTOGRAM | "histogram" | | | IP | "ip" | | | IP\_RANGE | "ip_range" | | +| MISSING | "missing" | | | MURMUR3 | "murmur3" | | | NESTED | "nested" | | | NUMBER | "number" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md index be4c3705bd8de..40fa872ff0fc6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md @@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES | HISTOGRAM | "histogram" | | | IP | "ip" | | | IP\_RANGE | "ip_range" | | +| MISSING | "missing" | | | MURMUR3 | "murmur3" | | | NESTED | "nested" | | | NUMBER | "number" | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md index 5201444e69867..290dc10662569 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md @@ -9,9 +9,9 @@ Merges input$ and output$ streams and debounces emit till next macro-task. Could Signature: ```typescript -getUpdated$(): Readonly>; +getUpdated$(): Readonly>; ``` Returns: -`Readonly>` +`Readonly>` diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts index c46e5c5266f55..e6f815e058ce3 100644 --- a/src/plugins/data/common/kbn_field_types/types.ts +++ b/src/plugins/data/common/kbn_field_types/types.ts @@ -80,4 +80,5 @@ export enum KBN_FIELD_TYPES { OBJECT = 'object', NESTED = 'nested', HISTOGRAM = 'histogram', + MISSING = 'missing', } diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 297af560081b1..3ce528e6ed893 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -230,7 +230,7 @@ describe('AggConfigs', () => { describe('#toDsl', () => { beforeEach(() => { indexPattern = stubIndexPattern as IndexPattern; - indexPattern.fields.getByName = (name) => (name as unknown) as IndexPatternField; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); }); it('uses the sorted aggs', () => { diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 4e278d5872a3e..56e720d237c45 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -16,16 +16,33 @@ import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketAggConfig } from './bucket_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; +import type { IndexPatternField } from '../../../index_patterns'; +import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern'; const indexPattern = { id: '1234', title: 'logstash-*', fields: [ { - name: 'field', + name: 'machine.os.raw', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'geo.src', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, }, ], -} as any; +} as IndexPattern; + +indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); const singleTerm = { aggs: [ diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index bb34d7ede453c..09dfbb28a4e53 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -10,6 +10,8 @@ import { AggConfigs } from '../agg_configs'; import { METRIC_TYPES } from '../metrics'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; +import type { IndexPatternField } from '../../../index_patterns'; +import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern'; describe('Terms Agg', () => { describe('order agg editor UI', () => { @@ -17,16 +19,44 @@ describe('Terms Agg', () => { const indexPattern = { id: '1234', title: 'logstash-*', - fields: { - getByName: () => field, - filter: () => [field], - }, - } as any; + fields: [ + { + name: 'field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'string_field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'empty_number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + } as IndexPattern; - const field = { - name: 'field', - indexPattern, - }; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); + indexPattern.fields.filter = () => indexPattern.fields; return new AggConfigs( indexPattern, @@ -207,16 +237,28 @@ describe('Terms Agg', () => { const indexPattern = { id: '1234', title: 'logstash-*', - fields: { - getByName: () => field, - filter: () => [field], - }, - } as any; + fields: [ + { + name: 'string_field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + } as IndexPattern; - const field = { - name: 'field', - indexPattern, - }; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); + indexPattern.fields.filter = () => indexPattern.fields; const aggConfigs = new AggConfigs( indexPattern, diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 2d3ff8f5fdba8..62dac9831211a 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n'; import { IAggConfig } from '../agg_config'; -import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common'; +import { + SavedFieldNotFound, + SavedFieldTypeInvalidForAgg, +} from '../../../../../../plugins/kibana_utils/common'; import { BaseParamType } from './base'; import { propFilter } from '../utils'; import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types'; @@ -47,13 +50,49 @@ export class FieldParamType extends BaseParamType { ); } - if (field.scripted) { + if (field.type === KBN_FIELD_TYPES.MISSING) { + throw new SavedFieldNotFound( + i18n.translate( + 'data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage', + { + defaultMessage: + 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.', + values: { + fieldParameter: field.name, + }, + } + ) + ); + } + + const validField = this.getAvailableFields(aggConfig).find( + (f: any) => f.name === field.name + ); + + if (!validField) { + throw new SavedFieldTypeInvalidForAgg( + i18n.translate( + 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', + { + defaultMessage: + 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', + values: { + fieldParameter: field.name, + aggType: aggConfig?.type?.title, + indexPatternTitle: aggConfig.getIndexPattern().title, + }, + } + ) + ); + } + + if (validField.scripted) { output.params.script = { - source: field.script, - lang: field.lang, + source: validField.script, + lang: validField.lang, }; } else { - output.params.field = field.name; + output.params.field = validField.name; } }; } @@ -69,28 +108,15 @@ export class FieldParamType extends BaseParamType { const field = aggConfig.getIndexPattern().fields.getByName(fieldName); if (!field) { - throw new SavedObjectNotFound('index-pattern-field', fieldName); - } - - const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); - if (!validField) { - throw new Error( - i18n.translate( - 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', - { - defaultMessage: - 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', - values: { - fieldParameter: fieldName, - aggType: aggConfig?.type?.title, - indexPatternTitle: aggConfig.getIndexPattern().title, - }, - } - ) - ); + return new IndexPatternField({ + type: KBN_FIELD_TYPES.MISSING, + name: fieldName, + searchable: false, + aggregatable: false, + }); } - return validField; + return field; }; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index db7f814a83f79..05925f097de24 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1786,6 +1786,8 @@ export enum KBN_FIELD_TYPES { // (undocumented) IP_RANGE = "ip_range", // (undocumented) + MISSING = "missing", + // (undocumented) MURMUR3 = "murmur3", // (undocumented) NESTED = "nested", diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 9fff4ac95c87e..053b60956fa92 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1082,6 +1082,8 @@ export enum KBN_FIELD_TYPES { // (undocumented) IP_RANGE = "ip_range", // (undocumented) + MISSING = "missing", + // (undocumented) MURMUR3 = "murmur3", // (undocumented) NESTED = "nested", diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index e8418970d83f7..a0cd213b7bf24 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -9,7 +9,7 @@ import { cloneDeep, isEqual } from 'lodash'; import * as Rx from 'rxjs'; import { merge } from 'rxjs'; -import { debounceTime, distinctUntilChanged, map, mapTo, skip } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators'; import { RenderCompleteDispatcher } from '../../../../kibana_utils/public'; import { Adapters } from '../types'; import { IContainer } from '../containers'; @@ -111,10 +111,9 @@ export abstract class Embeddable< * In case corresponding state change triggered `reload` this stream is guarantied to emit later, * which allows to skip any state handling in case `reload` already handled it. */ - public getUpdated$(): Readonly> { + public getUpdated$(): Readonly> { return merge(this.getInput$().pipe(skip(1)), this.getOutput$().pipe(skip(1))).pipe( - debounceTime(0), - mapTo(undefined) + debounceTime(0) ); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index b9719542adc81..3f0907acabdfa 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -282,7 +282,7 @@ export abstract class Embeddable>; + getUpdated$(): Readonly>; // (undocumented) readonly id: string; // (undocumented) diff --git a/src/plugins/kibana_utils/common/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts index 7a9495cc8f413..7f3efc6d9571f 100644 --- a/src/plugins/kibana_utils/common/errors/errors.ts +++ b/src/plugins/kibana_utils/common/errors/errors.ts @@ -32,7 +32,7 @@ export class DuplicateField extends KbnError { export class SavedObjectNotFound extends KbnError { public savedObjectType: string; public savedObjectId?: string; - constructor(type: string, id?: string, link?: string) { + constructor(type: string, id?: string, link?: string, customMessage?: string) { const idMsg = id ? ` (id: ${id})` : ''; let message = `Could not locate that ${type}${idMsg}`; @@ -40,13 +40,31 @@ export class SavedObjectNotFound extends KbnError { message += `, [click here to re-create it](${link})`; } - super(message); + super(customMessage || message); this.savedObjectType = type; this.savedObjectId = id; } } +/** + * A saved field doesn't exist anymore + */ +export class SavedFieldNotFound extends KbnError { + constructor(message: string) { + super(message); + } +} + +/** + * A saved field type isn't compatible with aggregation + */ +export class SavedFieldTypeInvalidForAgg extends KbnError { + constructor(message: string) { + super(message); + } +} + /** * This error is for scenarios where a saved object is detected that has invalid JSON properties. * There was a scenario where we were importing objects with double-encoded JSON, and the system diff --git a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx index 94f767510c4bd..277804567c2b7 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx @@ -11,7 +11,7 @@ import { act } from 'react-dom/test-utils'; import { mount, shallow, ReactWrapper } from 'enzyme'; import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui'; -import { IAggConfig, IndexPatternField } from 'src/plugins/data/public'; +import { IAggConfig, IndexPatternField, AggParam } from 'src/plugins/data/public'; import { ComboBoxGroupedOptions } from '../../utils'; import { FieldParamEditor, FieldParamEditorProps } from './field'; import { EditorVisState } from '../sidebar/state/reducers'; @@ -42,7 +42,7 @@ describe('FieldParamEditor component', () => { setTouched = jest.fn(); onChange = jest.fn(); - field = { displayName: 'bytes' } as IndexPatternField; + field = { displayName: 'bytes', type: 'bytes' } as IndexPatternField; option = { label: 'bytes', target: field }; indexedFields = [ { @@ -52,7 +52,16 @@ describe('FieldParamEditor component', () => { ]; defaultProps = { - agg: {} as IAggConfig, + agg: { + type: { + params: [ + ({ + name: 'field', + filterFieldTypes: ['bytes'], + } as unknown) as AggParam, + ], + }, + } as IAggConfig, aggParam: { name: 'field', type: 'field', diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index 95843dc6ae3a8..f8db2d89888a2 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -13,7 +13,13 @@ import useMount from 'react-use/lib/useMount'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggParam, IAggConfig, IFieldParamType, IndexPatternField } from 'src/plugins/data/public'; +import { + AggParam, + IAggConfig, + IFieldParamType, + IndexPatternField, + KBN_FIELD_TYPES, +} from '../../../../../plugins/data/public'; import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { ComboBoxGroupedOptions } from '../../utils'; @@ -55,6 +61,7 @@ function FieldParamEditor({ } }; const errors = customError ? [customError] : []; + let showErrorMessageImmediately = false; if (!indexedFields.length) { errors.push( @@ -69,9 +76,38 @@ function FieldParamEditor({ ); } + if (value && value.type === KBN_FIELD_TYPES.MISSING) { + errors.push( + i18n.translate('visDefaultEditor.controls.field.fieldIsNotExists', { + defaultMessage: + 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.', + values: { + fieldParameter: value.name, + }, + }) + ); + showErrorMessageImmediately = true; + } else if ( + value && + !getFieldTypes(agg).find((type: string) => type === value.type || type === '*') + ) { + errors.push( + i18n.translate('visDefaultEditor.controls.field.invalidFieldForAggregation', { + defaultMessage: + 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with this aggregation. Please select a new field.', + values: { + fieldParameter: value?.name, + indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title, + }, + }) + ); + showErrorMessageImmediately = true; + } + const isValid = !!value && !errors.length && !isDirty; // we show an error message right away if there is no compatible fields - const showErrorMessage = (showValidation || !indexedFields.length) && !isValid; + const showErrorMessage = + (showValidation || !indexedFields.length || showErrorMessageImmediately) && !isValid; useValidation(setValidity, isValid); useMount(() => { @@ -122,10 +158,14 @@ function FieldParamEditor({ } function getFieldTypesString(agg: IAggConfig) { + return formatListAsProse(getFieldTypes(agg), { inclusive: false }); +} + +function getFieldTypes(agg: IAggConfig) { const param = get(agg, 'type.params', []).find((p: AggParam) => p.name === 'field') || ({} as IFieldParamType); - return formatListAsProse(parseCommaSeparatedList(param.filterFieldTypes), { inclusive: false }); + return parseCommaSeparatedList(param.filterFieldTypes || []); } export { FieldParamEditor }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index efb166c8975bb..3bb52eb15758a 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -149,8 +149,9 @@ export class VisualizeEmbeddable } this.subscriptions.push( - this.getUpdated$().subscribe(() => { + this.getUpdated$().subscribe((value) => { const isDirty = this.handleChanges(); + if (isDirty && this.handler) { this.updateHandler(); } diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index cc0f3ce2afae5..9eda709e58c3e 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -18,8 +18,17 @@ import { SavedObject } from 'src/plugins/saved_objects/public'; import { cloneDeep } from 'lodash'; import { ExpressionValueError } from 'src/plugins/expressions/public'; import { createSavedSearchesLoader } from '../../../../discover/public'; +import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common'; import { VisualizeServices } from '../types'; +function isErrorRelatedToRuntimeFields(error: ExpressionValueError['error']) { + const originalError = error.original || error; + return ( + originalError instanceof SavedFieldNotFound || + originalError instanceof SavedFieldTypeInvalidForAgg + ); +} + const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices @@ -37,7 +46,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( })) as VisualizeEmbeddableContract; embeddableHandler.getOutput$().subscribe((output) => { - if (output.error) { + if (output.error && !isErrorRelatedToRuntimeFields(output.error)) { data.search.showError( ((output.error as unknown) as ExpressionValueError['error']).original || output.error ); diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 64d61996495d7..965951bfbd88d 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -11,13 +11,12 @@ import { EventEmitter } from 'events'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; -import { redirectWhenMissing } from '../../../../../kibana_utils/public'; - import { getVisualizationInstance } from '../get_visualization_instance'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; import { getVisEditorsRegistry } from '../../../services'; +import { redirectToSavedObjectPage } from '../utils'; /** * This effect is responsible for instantiating a saved vis or creating a new one @@ -43,9 +42,7 @@ export const useSavedVisInstance = ( chrome, history, dashboard, - setActiveUrl, toastNotifications, - http: { basePath }, stateTransferService, application: { navigateToApp }, } = services; @@ -131,27 +128,8 @@ export const useSavedVisInstance = ( visEditorController, }); } catch (error) { - const managementRedirectTarget = { - app: 'management', - path: `kibana/objects/savedVisualizations/${visualizationIdFromUrl}`, - }; - try { - redirectWhenMissing({ - history, - navigateToApp, - toastNotifications, - basePath, - mapping: { - visualization: VisualizeConstants.LANDING_PAGE_PATH, - search: managementRedirectTarget, - 'index-pattern': managementRedirectTarget, - 'index-pattern-field': managementRedirectTarget, - }, - onBeforeRedirect() { - setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH); - }, - })(error); + redirectToSavedObjectPage(services, error, visualizationIdFromUrl); } catch (e) { toastNotifications.addWarning({ title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts index 0e529507f97e3..c906ff5304c90 100644 --- a/src/plugins/visualize/public/application/utils/utils.ts +++ b/src/plugins/visualize/public/application/utils/utils.ts @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ChromeStart, DocLinksStart } from 'kibana/public'; import { Filter } from '../../../../data/public'; +import { redirectWhenMissing } from '../../../../kibana_utils/public'; +import { VisualizeConstants } from '../visualize_constants'; import { VisualizeServices, VisualizeEditorVisInstance } from '../types'; export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => { @@ -58,3 +60,36 @@ export const visStateToEditorState = ( linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId, }; }; + +export const redirectToSavedObjectPage = ( + services: VisualizeServices, + error: any, + savedVisualizationsId?: string +) => { + const { + history, + setActiveUrl, + toastNotifications, + http: { basePath }, + application: { navigateToApp }, + } = services; + const managementRedirectTarget = { + app: 'management', + path: `kibana/objects/savedVisualizations/${savedVisualizationsId}`, + }; + redirectWhenMissing({ + history, + navigateToApp, + toastNotifications, + basePath, + mapping: { + visualization: VisualizeConstants.LANDING_PAGE_PATH, + search: managementRedirectTarget, + 'index-pattern': managementRedirectTarget, + 'index-pattern-field': managementRedirectTarget, + }, + onBeforeRedirect() { + setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH); + }, + })(error); +}; From de8ba08ac00045535a209af251cf40c837753b89 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 8 Apr 2021 10:44:31 -0400 Subject: [PATCH 113/131] [Security Solution][Rac][Tgrid] Use correct prop name for disabling timeline context menu button (#96453) * Use correct prop name for disabling timeline context menu button * Update usages of disabled to isDisabled on EuiIconButton * Update failing snapshot and tests looking for old prop --- .../add_to_case_action.test.tsx | 4 +- .../timeline_actions/add_to_case_action.tsx | 2 +- .../timeline_actions/alert_context_menu.tsx | 167 +++++++++--------- .../tooltip_footer.test.tsx.snap | 4 +- .../map_tool_tip/tooltip_footer.test.tsx | 32 ++-- .../map_tool_tip/tooltip_footer.tsx | 4 +- 6 files changed, 110 insertions(+), 103 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index c99cabb50e3dc..40a202f5257a7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -341,7 +341,7 @@ describe('AddToCaseAction', () => { ); expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') ).toBeTruthy(); }); @@ -358,7 +358,7 @@ describe('AddToCaseAction', () => { ); expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index decd37a7646e7..45c1355cecfa7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -172,7 +172,7 @@ const AddToCaseActionComponent: React.FC = ({ size="s" iconType="folderClosed" onClick={openPopover} - disabled={isDisabled} + isDisabled={isDisabled} /> ), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index b2e5638ff120e..26b9662a8f19b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -215,19 +215,20 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const openAlertActionComponent = ( - - {i18n.ACTION_OPEN_ALERT} - - ); + const openAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_OPEN_ALERT} + + ); + }, [openAlertActionOnClick, hasIndexUpdateDelete, hasIndexMaintenance]); const closeAlertActionClick = useCallback(() => { updateAlertStatusAction({ @@ -248,19 +249,20 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const closeAlertActionComponent = ( - - {i18n.ACTION_CLOSE_ALERT} - - ); + const closeAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_CLOSE_ALERT} + + ); + }, [closeAlertActionClick, hasIndexUpdateDelete, hasIndexMaintenance]); const inProgressAlertActionClick = useCallback(() => { updateAlertStatusAction({ @@ -281,72 +283,77 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const inProgressAlertActionComponent = ( - - {i18n.ACTION_IN_PROGRESS_ALERT} - - ); - - const button = ( - - - - ); + const inProgressAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_IN_PROGRESS_ALERT} + + ); + }, [canUserCRUD, hasIndexUpdateDelete, inProgressAlertActionClick]); + + const button = useMemo(() => { + return ( + + + + ); + }, [disabled, onButtonClick, ariaLabel]); const handleAddEndpointExceptionClick = useCallback((): void => { closePopover(); setOpenAddExceptionModal('endpoint'); }, [closePopover]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const addEndpointExceptionComponent = ( - - {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} - - ); + const addEndpointExceptionComponent = useMemo(() => { + return ( + + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + + ); + }, [canUserCRUD, hasIndexWrite, isEndpointAlert, handleAddEndpointExceptionClick]); const handleAddExceptionClick = useCallback((): void => { closePopover(); setOpenAddExceptionModal('detection'); }, [closePopover]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const addExceptionComponent = ( - - - {i18n.ACTION_ADD_EXCEPTION} - - - ); + const addExceptionComponent = useMemo(() => { + return ( + + + {i18n.ACTION_ADD_EXCEPTION} + + + ); + }, [handleAddExceptionClick, canUserCRUD, hasIndexWrite]); const statusFilters = useMemo(() => { if (!alertStatus) { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap index 8db9006da6156..1b91d396bee30 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap @@ -27,16 +27,16 @@ exports[`ToolTipFilter renders correctly against snapshot 1`] = ` aria-label="Next" color="text" data-test-subj="previous-feature-button" - disabled={true} iconType="arrowLeft" + isDisabled={true} onClick={[MockFunction]} /> diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx index a74022a222528..cc7662cf1e960 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx @@ -42,7 +42,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(true); }); @@ -70,9 +70,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - false - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(false); }); test('nextFeature is called when featureIndex is < totalFeatures', () => { @@ -102,7 +102,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(false); }); @@ -130,9 +130,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - true - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(true); }); test('nextFunction is not called when featureIndex >== totalFeatures', () => { @@ -161,7 +161,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(true); }); @@ -189,9 +189,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - true - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(true); }); test('nextFunction is not called when only a single feature is provided', () => { @@ -221,7 +221,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(false); }); @@ -249,9 +249,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - false - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(false); }); test('nextFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx index 252260b2c5a2b..dbb280228e504 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx @@ -54,7 +54,7 @@ export const ToolTipFooterComponent = ({ onClick={previousFeature} iconType="arrowLeft" aria-label="Next" - disabled={featureIndex <= 0} + isDisabled={featureIndex <= 0} /> = totalFeatures - 1} + isDisabled={featureIndex >= totalFeatures - 1} /> From 158932e37773f0b8560073b5775266b1ed291c4b Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 8 Apr 2021 18:03:55 +0300 Subject: [PATCH 114/131] [Visualizations] Fixes chart visibility on Safari small screens (#96276) * [Visualizations] Fix chart visibility on Safari small screens * Fix safari bug without changing the chrome behavior * fix layout inconsistencies across browsers Co-authored-by: Michael Marcialis Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_default_editor/public/_default.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss index c412b9d915e55..56c6a0f0f63f6 100644 --- a/src/plugins/vis_default_editor/public/_default.scss +++ b/src/plugins/vis_default_editor/public/_default.scss @@ -1,6 +1,4 @@ .visEditor--default { - // height: 1px is in place to make editor children take their height in the parent - height: 1px; flex: 1 1 auto; display: flex; } @@ -80,6 +78,7 @@ .visEditor__collapsibleSidebar { width: 100% !important; // force the editor to take 100% width + flex-grow: 0; } .visEditor__collapsibleSidebar-isClosed { @@ -91,8 +90,10 @@ } .visEditor__visualization__wrapper { - // force the visualization to take 100% width and height. + // force the visualization to take 100% width. width: 100% !important; - height: 100% !important; + flex: 1; + display: flex; + flex-direction: column; } } From 8658ef49d80e191f615950e2d8f427182fff4294 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:06:47 -0400 Subject: [PATCH 115/131] [K8] Continuing to fix the KQL bar styles (#96264) --- .../ui/query_string_input/_query_bar.scss | 20 ++++++++++++++++++- .../query_string_input/query_string_input.tsx | 11 ++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) 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 466cc8c3de0b7..4e12f11668734 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 @@ -17,6 +17,16 @@ @include kbnThemeStyle('v8') { background-color: $euiFormBackgroundColor; + border-radius: $euiFormControlBorderRadius; + + &.kbnQueryBar__textareaWrap--hasPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &.kbnQueryBar__textareaWrap--hasAppend { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } } @@ -35,8 +45,16 @@ } @include kbnThemeStyle('v8') { - border-radius: 0; padding-bottom: $euiSizeS + 1px; + + &.kbnQueryBar__textarea--hasPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &.kbnQueryBar__textarea--hasAppend { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } &:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) { diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 900a4ab7d7eb7..0f660f87266fd 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -682,7 +682,14 @@ export default class QueryStringInputUI extends Component { ); const inputClassName = classNames( 'kbnQueryBar__textarea', - this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null + this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null, + this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null, + !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null + ); + const inputWrapClassName = classNames( + 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap', + this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null, + !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null ); return ( @@ -711,7 +718,7 @@ export default class QueryStringInputUI extends Component { >

Date: Thu, 8 Apr 2021 08:13:51 -0700 Subject: [PATCH 116/131] skip entire fleet_api_integration suite to unblock es promotion (#96515) --- x-pack/scripts/functional_tests.js | 3 ++- x-pack/test/fleet_api_integration/apis/agents_setup.ts | 3 +-- x-pack/test/fleet_api_integration/apis/epm/list.ts | 3 +-- x-pack/test/fleet_api_integration/apis/fleet_setup.ts | 3 +-- .../security_solution_endpoint_api_int/apis/artifacts/index.ts | 3 +-- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 90306466a9753..6321aa8880587 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -73,7 +73,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), require.resolve('../test/security_solution_endpoint_api_int/config.ts'), - require.resolve('../test/fleet_api_integration/config.ts'), + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + // require.resolve('../test/fleet_api_integration/config.ts'), require.resolve('../test/search_sessions_integration/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts index d49bc91251b01..91d6ca0119d1d 100644 --- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts @@ -15,8 +15,7 @@ export default function (providerContext: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 - describe.skip('fleet_agents_setup', () => { + describe('fleet_agents_setup', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/fleet_api_integration/apis/epm/list.ts b/x-pack/test/fleet_api_integration/apis/epm/list.ts index 0a7002764a54c..5a991e52bdba4 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/list.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/list.ts @@ -19,8 +19,7 @@ export default function (providerContext: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 - describe.skip('EPM - list', async function () { + describe('EPM - list', async function () { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('fleet/empty_fleet_server'); diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index a82ed3f8cf22d..c9709475d182d 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -15,8 +15,7 @@ export default function (providerContext: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 - describe.skip('fleet_setup', () => { + describe('fleet_setup', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index 8ee028ae3f56b..e1edeb7808697 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -19,8 +19,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 - describe.skip('artifact download', () => { + describe('artifact download', () => { const esArchiverSnapshots = [ 'endpoint/artifacts/fleet_artifacts', 'endpoint/artifacts/api_feature', From 0d62e11736faef221cd7d1f0044d862255fe0d34 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 8 Apr 2021 17:23:36 +0200 Subject: [PATCH 117/131] Document SO migrations enableV2 and batchSize config options (#96290) * document SO batchsize and migrationsv2 enablement options * use refs by name * Update docs/setup/settings.asciidoc Co-authored-by: Luke Elmers * apply Lukes suggestion * add a note that migrations.enableV2 will be removed soon * document migrations.retryAttempts * Apply suggestions from code review Co-authored-by: Kaarina Tungseth Co-authored-by: Luke Elmers Co-authored-by: Kaarina Tungseth --- docs/setup/settings.asciidoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 643718b961650..90e813afad6f4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -429,6 +429,15 @@ to display map tiles in tilemap visualizations. By default, override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` +| `migrations.batchSize:` + | Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If the migration fails due to a `circuit_breaking_exception`, set a smaller `batchSize` value. *Default: `1000`* + +| `migrations.enableV2:` + | experimental[]. Enables the new Saved Objects migration algorithm. For information about the migration algorithm, refer to <>. When `migrations v2` is stable, the setting will be removed in an upcoming release without any further notice. Setting the value to `false` causes {kib} to use the legacy migration algorithm, which shipped in 7.11 and earlier versions. *Default: `true`* + +| `migrations.retryAttempts:` + | The number of times migrations retry temporary failures, such as a network timeout, 503 status code, or `snapshot_in_progress_exception`. When upgrade migrations frequently fail after exhausting all retry attempts with a message such as `Unable to complete the [...] step after 15 attempts, terminating.`, increase the setting value. *Default: `15`* + | `newsfeed.enabled:` | Controls whether to enable the newsfeed system for the {kib} UI notification center. Set to `false` to disable the From c9d19fd43bf1149dfcaffc2f1fe7108e98ac57d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 8 Apr 2021 16:41:27 +0100 Subject: [PATCH 118/131] [ILM] Fix frozen phase min_age (de)serialization (#96544) --- .../policy_serialization.test.ts | 91 ++++++++++++++++--- .../sections/edit_policy/form/deserializer.ts | 8 ++ .../edit_policy/form/serializer/serializer.ts | 7 ++ 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 846e20b48ddca..aa176fe3b188f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -481,21 +481,84 @@ describe(' serialization', () => { }); }); - test('delete phase', async () => { - const { actions } = testBed; - await actions.delete.enable(true); - await actions.setWaitForSnapshotPolicy('test'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(entirePolicy.phases.delete).toEqual({ - min_age: '365d', - actions: { - delete: {}, - wait_for_snapshot: { - policy: 'test', + describe('frozen phase', () => { + test('default value', async () => { + const { actions } = testBed; + await actions.frozen.enable(true); + await actions.frozen.setSearchableSnapshot('myRepo'); + + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.frozen).toEqual({ + min_age: '0d', + actions: { + searchable_snapshot: { snapshot_repository: 'myRepo' }, }, - }, + }); + }); + + describe('deserialization', () => { + beforeEach(async () => { + const policyToEdit = getDefaultHotPhasePolicy('my_policy'); + policyToEdit.policy.phases.frozen = { + min_age: '1234m', + actions: { searchable_snapshot: { snapshot_repository: 'myRepo' } }, + }; + + httpRequestsMockHelpers.setLoadPolicies([policyToEdit]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default value', async () => { + const { actions } = testBed; + + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.frozen).toEqual({ + min_age: '1234m', + actions: { + searchable_snapshot: { + snapshot_repository: 'myRepo', + }, + }, + }); + }); + }); + }); + + describe('delete phase', () => { + test('default value', async () => { + const { actions } = testBed; + await actions.delete.enable(true); + await actions.setWaitForSnapshotPolicy('test'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.delete).toEqual({ + min_age: '365d', + actions: { + delete: {}, + wait_for_snapshot: { + policy: 'test', + }, + }, + }); }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 73ecb0d73b7a7..af571d16ca8c5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -114,6 +114,14 @@ export const createDeserializer = (isCloudEnabled: boolean) => ( } } + if (draft.phases.frozen) { + if (draft.phases.frozen.min_age) { + const minAge = splitSizeAndUnits(draft.phases.frozen.min_age); + draft.phases.frozen.min_age = minAge.size; + draft._meta.frozen.minAgeUnit = minAge.units; + } + } + if (draft.phases.delete) { if (draft.phases.delete.min_age) { const minAge = splitSizeAndUnits(draft.phases.delete.min_age); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 24dafa6cca237..0b1db784469a9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -267,6 +267,13 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {}; const frozenPhase = draft.phases.frozen!; + /** + * FROZEN PHASE MIN AGE + */ + if (updatedPolicy.phases.frozen?.min_age) { + frozenPhase.min_age = `${updatedPolicy.phases.frozen!.min_age}${_meta.frozen.minAgeUnit}`; + } + /** * FROZEN PHASE SEARCHABLE SNAPSHOT */ From 6b4becfe02357a091318c3e7930cc3cafdf0eb16 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 8 Apr 2021 17:46:45 +0200 Subject: [PATCH 119/131] [ML] Data Frame Analytics: Don't allow user to pick an index pattern or saved search based on CCS. (#96555) Data Frame Analytics does not support cross-cluster search. This PR fixes the SourceSelection component to not allow a user to select a CCS index pattern or a saved search using a CCS index pattern. --- .../source_selection.test.tsx | 216 ++++++++++++++++++ .../source_selection/source_selection.tsx | 78 ++++++- 2 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx new file mode 100644 index 0000000000000..858ab58b53f4b --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; + +import { IntlProvider } from 'react-intl'; + +import { + getIndexPatternAndSavedSearch, + IndexPatternAndSavedSearch, +} from '../../../../../util/index_utils'; + +import { SourceSelection } from './source_selection'; + +jest.mock('../../../../../../../../../../src/plugins/saved_objects/public', () => { + const SavedObjectFinderUi = ({ + onChoose, + }: { + onChoose: (id: string, type: string, fullName: string, savedObject: object) => void; + }) => { + return ( + <> + + + + + + ); + }; + + return { + SavedObjectFinderUi, + }; +}); + +const mockNavigateToPath = jest.fn(); +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: { + savedObjects: {}, + uiSettings: {}, + }, + }), + useNavigateToPath: () => mockNavigateToPath, +})); + +jest.mock('../../../../../util/index_utils', () => { + return { + getIndexPatternAndSavedSearch: jest.fn( + async (id: string): Promise => { + return { + indexPattern: { + fields: [], + title: + id === 'the-remote-saved-search-id' + ? 'my_remote_cluster:index-pattern-title' + : 'index-pattern-title', + }, + savedSearch: null, + }; + } + ), + }; +}); + +const mockOnClose = jest.fn(); +const mockGetIndexPatternAndSavedSearch = getIndexPatternAndSavedSearch as jest.Mock; + +describe('Data Frame Analytics: ', () => { + afterEach(() => { + mockNavigateToPath.mockClear(); + mockGetIndexPatternAndSavedSearch.mockClear(); + }); + + it('renders the title text', async () => { + // prepare + render( + + + + ); + + // assert + expect(screen.queryByText('New analytics job')).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + + it('shows the error callout when clicking a remote index pattern', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('RemoteIndexPattern', { selector: 'button' })); + await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut')); + + // assert + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + + it('calls navigateToPath for a plain index pattern ', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('PlainIndexPattern', { selector: 'button' })); + + // assert + await waitFor(() => { + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).not.toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledWith( + '/data_frame_analytics/new_job?index=the-plain-index-pattern-id' + ); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + }); + + it('shows the error callout when clicking a saved search using a remote index pattern', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('RemoteSavedSearch', { selector: 'button' })); + await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut')); + + // assert + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).toBeInTheDocument(); + expect( + screen.queryByText( + `The saved search 'the-remote-saved-search-title' uses the index pattern 'my_remote_cluster:index-pattern-title'.` + ) + ).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-remote-saved-search-id'); + }); + + it('calls navigateToPath for a saved search using a plain index pattern ', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('PlainSavedSearch', { selector: 'button' })); + + // assert + await waitFor(() => { + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).not.toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledWith( + '/data_frame_analytics/new_job?savedSearchId=the-plain-saved-search-id' + ); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-plain-saved-search-id'); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index 40f97690d7790..cbc5a226eb319 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -5,15 +5,28 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; +import { + EuiCallOut, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, +} from '@elastic/eui'; + +import type { SimpleSavedObject } from 'src/core/public'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; +import { getNestedProperty } from '../../../../../util/object_utils'; + +import { getIndexPatternAndSavedSearch } from '../../../../../util/index_utils'; + const fixedPageSize: number = 8; interface Props { @@ -26,7 +39,49 @@ export const SourceSelection: FC = ({ onClose }) => { } = useMlKibana(); const navigateToPath = useNavigateToPath(); - const onSearchSelected = async (id: string, type: string) => { + const [isCcsCallOut, setIsCcsCallOut] = useState(false); + const [ccsCallOutBodyText, setCcsCallOutBodyText] = useState(); + + const onSearchSelected = async ( + id: string, + type: string, + fullName: string, + savedObject: SimpleSavedObject + ) => { + // Kibana index patterns including `:` are cross-cluster search indices + // and are not supported by Data Frame Analytics yet. For saved searches + // and index patterns that use cross-cluster search we intercept + // the selection before redirecting and show an error callout instead. + let indexPatternTitle = ''; + + if (type === 'index-pattern') { + indexPatternTitle = getNestedProperty(savedObject, 'attributes.title'); + } else if (type === 'search') { + const indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(id); + indexPatternTitle = indexPatternAndSavedSearch.indexPattern?.title ?? ''; + } + + if (indexPatternTitle.includes(':')) { + setIsCcsCallOut(true); + if (type === 'search') { + setCcsCallOutBodyText( + i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutBody', + { + defaultMessage: `The saved search '{savedSearchTitle}' uses the index pattern '{indexPatternTitle}'.`, + values: { + savedSearchTitle: getNestedProperty(savedObject, 'attributes.title'), + indexPatternTitle, + }, + } + ) + ); + } else { + setCcsCallOutBodyText(undefined); + } + return; + } + await navigateToPath( `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' @@ -54,6 +109,23 @@ export const SourceSelection: FC = ({ onClose }) => { + {isCcsCallOut && ( + <> + + {typeof ccsCallOutBodyText === 'string' &&

{ccsCallOutBodyText}

} +
+ + + )} Date: Thu, 8 Apr 2021 11:05:13 -0500 Subject: [PATCH 120/131] skip flaky test. #77933 --- x-pack/test/accessibility/apps/spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 032186b2e90ec..41926628c2377 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -24,7 +24,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('home'); }); - it('a11y test for manage spaces menu from top nav on Kibana home', async () => { + // flaky https://github.com/elastic/kibana/issues/77933 + it.skip('a11y test for manage spaces menu from top nav on Kibana home', async () => { await PageObjects.spaceSelector.openSpacesNav(); await retry.waitFor( 'Manage spaces option visible', From 2582522c66815f0aad93e5e508421b8eb4c50ac4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:08:45 -0500 Subject: [PATCH 121/131] [ML] Stabilize Anomaly Explorer embeddable functional tests (#96421) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts | 1 + .../apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index 1761c44813430..deb91f6b9b1ef 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -97,6 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); await a11y.testAppSnapshot(); diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts index 1619765946916..f7bfd7f7a4c62 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -88,6 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); }); From 955c46ba5e8ff24bc55774a5513718076f0b06cb Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:15:27 -0500 Subject: [PATCH 122/131] [ML] Add ML plugin contract mocks (#96265) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/ml_url_generator.ts | 17 +++++++++++ x-pack/plugins/ml/public/mocks.ts | 24 +++++++++++++++ x-pack/plugins/ml/server/mocks.ts | 30 +++++++++++++++++++ .../providers/__mocks__/alerting_service.ts | 11 +++++++ .../providers/__mocks__/anomaly_detectors.ts | 14 +++++++++ .../providers/__mocks__/jobs_service.ts | 11 +++++++ .../providers/__mocks__/modules.ts | 14 +++++++++ .../providers/__mocks__/results_service.ts | 11 +++++++ .../providers/__mocks__/system.ts | 13 ++++++++ .../rules/create_rules_bulk_route.test.ts | 4 +-- .../routes/rules/create_rules_route.test.ts | 4 +-- .../routes/rules/import_rules_route.test.ts | 4 +-- .../rules/patch_rules_bulk_route.test.ts | 4 +-- .../routes/rules/patch_rules_route.test.ts | 4 +-- .../rules/update_rules_bulk_route.test.ts | 4 +-- .../routes/rules/update_rules_route.test.ts | 4 +-- .../server/lib/machine_learning/authz.test.ts | 8 ++--- .../server/lib/machine_learning/mocks.ts | 20 ++----------- .../usage/detections/detections.test.ts | 6 ++-- 19 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.ts create mode 100644 x-pack/plugins/ml/public/mocks.ts create mode 100644 x-pack/plugins/ml/server/mocks.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts create mode 100644 x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.ts diff --git a/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.ts new file mode 100644 index 0000000000000..e5c6a2345e167 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; + +export const createMlUrlGeneratorMock = () => + ({ + id: ML_APP_URL_GENERATOR, + isDeprecated: false, + createUrl: jest.fn(), + migrate: jest.fn(), + } as jest.Mocked>); diff --git a/x-pack/plugins/ml/public/mocks.ts b/x-pack/plugins/ml/public/mocks.ts new file mode 100644 index 0000000000000..6b55cb3b6b650 --- /dev/null +++ b/x-pack/plugins/ml/public/mocks.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createMlUrlGeneratorMock } from './ml_url_generator/__mocks__/ml_url_generator'; +import { MlPluginSetup, MlPluginStart } from './plugin'; +const createSetupContract = (): jest.Mocked => { + return { + urlGenerator: createMlUrlGeneratorMock(), + }; +}; + +const createStartContract = (): jest.Mocked => { + return { + urlGenerator: createMlUrlGeneratorMock(), + }; +}; + +export const mlPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/ml/server/mocks.ts b/x-pack/plugins/ml/server/mocks.ts new file mode 100644 index 0000000000000..e50f78a0fd99d --- /dev/null +++ b/x-pack/plugins/ml/server/mocks.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createJobServiceProviderMock } from './shared_services/providers/__mocks__/jobs_service'; +import { createAnomalyDetectorsProviderMock } from './shared_services/providers/__mocks__/anomaly_detectors'; +import { createMockMlSystemProvider } from './shared_services/providers/__mocks__/system'; +import { createModulesProviderMock } from './shared_services/providers/__mocks__/modules'; +import { createResultsServiceProviderMock } from './shared_services/providers/__mocks__/results_service'; +import { createAlertingServiceProviderMock } from './shared_services/providers/__mocks__/alerting_service'; +import { MlPluginSetup } from './plugin'; + +const createSetupContract = () => + (({ + jobServiceProvider: createJobServiceProviderMock(), + anomalyDetectorsProvider: createAnomalyDetectorsProviderMock(), + mlSystemProvider: createMockMlSystemProvider(), + modulesProvider: createModulesProviderMock(), + resultsServiceProvider: createResultsServiceProviderMock(), + alertingServiceProvider: createAlertingServiceProviderMock(), + } as unknown) as jest.Mocked); + +const createStartContract = () => jest.fn(); + +export const mlPluginServerMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts new file mode 100644 index 0000000000000..957321e61b83a --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const createAlertingServiceProviderMock = () => + jest.fn(() => ({ + preview: jest.fn(), + execute: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts new file mode 100644 index 0000000000000..12b12e4ba06df --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createAnomalyDetectorsProviderMock = () => + jest.fn(() => ({ + jobs: jest.fn(), + jobStats: jest.fn(), + datafeeds: jest.fn(), + datafeedStats: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts new file mode 100644 index 0000000000000..e39373d66eff8 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createJobServiceProviderMock = () => + jest.fn(() => ({ + jobsSummary: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts new file mode 100644 index 0000000000000..b33e11dae5879 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createModulesProviderMock = () => + jest.fn(() => ({ + recognize: jest.fn(), + getModule: jest.fn(), + listModules: jest.fn(), + setup: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts new file mode 100644 index 0000000000000..7fd60d0b3428d --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createResultsServiceProviderMock = () => + jest.fn(() => ({ + getAnomaliesTableData: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.ts new file mode 100644 index 0000000000000..c002ddc4ced52 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createMockMlSystemProvider = () => + jest.fn(() => ({ + mlCapabilities: jest.fn(), + mlInfo: jest.fn(), + mlAnomalySearch: jest.fn(), + })); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index c5cbbeb09ed6d..ef7236084508d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -27,12 +27,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('create_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index dd636d5a180d9..d6693dc1f7a0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -29,12 +29,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('create_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 0a265adf620ee..b0b4232651803 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -34,7 +34,7 @@ describe('import_rules_route', () => { let server: ReturnType; let request: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); @@ -42,7 +42,7 @@ describe('import_rules_route', () => { config = createMockConfig(); const hapiStream = buildHapiStream(ruleIdsToNdJsonString(['rule-1'])); request = getImportRulesRequest(hapiStream); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 88250fb920d6c..93fdf9c5f8194 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -24,12 +24,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.alertsClient.update.mockResolvedValue(getResult()); // update succeeds diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 1f21a11f22ef5..6e62f65f44858 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -26,12 +26,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 09ac156c375ee..41b31b04e3424 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -26,12 +26,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.update.mockResolvedValue(getResult()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index e5bea42bc49a1..c80d32e09ccab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -28,12 +28,12 @@ jest.mock('../../rules/update_rules_notifications'); describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts index b41ba543675ec..d87c53ecfba71 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts @@ -16,7 +16,7 @@ jest.mock('../../../common/machine_learning/has_ml_admin_permissions'); describe('isMlAdmin', () => { it('returns true if hasMlAdminPermissions is true', async () => { - const mockMl = mlServicesMock.create(); + const mockMl = mlServicesMock.createSetupContract(); const request = httpServerMock.createKibanaRequest(); const savedObjectsClient = savedObjectsClientMock.create(); (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); @@ -25,7 +25,7 @@ describe('isMlAdmin', () => { }); it('returns false if hasMlAdminPermissions is false', async () => { - const mockMl = mlServicesMock.create(); + const mockMl = mlServicesMock.createSetupContract(); const request = httpServerMock.createKibanaRequest(); const savedObjectsClient = savedObjectsClientMock.create(); (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); @@ -56,13 +56,13 @@ describe('hasMlLicense', () => { describe('mlAuthz', () => { let licenseMock: ReturnType; - let mlMock: ReturnType; + let mlMock: ReturnType; let request: KibanaRequest; let savedObjectsClient: SavedObjectsClientContract; beforeEach(() => { licenseMock = licensingMock.createLicenseMock(); - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); request = httpServerMock.createKibanaRequest(); savedObjectsClient = savedObjectsClientMock.create(); }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts index 5d1b090e98a79..a121a682d2892 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts @@ -5,25 +5,9 @@ * 2.0. */ -import { MlPluginSetup } from '../../../../ml/server'; -import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { mlPluginServerMock } from '../../../../ml/server/mocks'; -const createMockClient = () => elasticsearchServiceMock.createLegacyClusterClient(); -const createMockMlSystemProvider = () => - jest.fn(() => ({ - mlCapabilities: jest.fn(), - })); - -export const mlServicesMock = { - create: () => - (({ - modulesProvider: jest.fn(), - jobServiceProvider: jest.fn(), - anomalyDetectorsProvider: jest.fn(), - mlSystemProvider: createMockMlSystemProvider(), - mlClient: createMockClient(), - } as unknown) as jest.Mocked), -}; +export const mlServicesMock = mlPluginServerMock; const mockValidateRuleType = jest.fn().mockResolvedValue({ valid: true, message: undefined }); const createBuildMlAuthzMock = () => diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index b53f90f40f621..64a33068ad686 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -21,12 +21,12 @@ import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index'; describe('Detections Usage and Metrics', () => { let esClientMock: jest.Mocked; let savedObjectsClientMock: jest.Mocked; - let mlMock: ReturnType; + let mlMock: ReturnType; describe('fetchDetectionsUsage()', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); }); it('returns zeroed counts if both calls are empty', async () => { @@ -108,7 +108,7 @@ describe('Detections Usage and Metrics', () => { describe('fetchDetectionsMetrics()', () => { beforeEach(() => { - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); }); it('returns an empty array if there is no data', async () => { From d904f8d1bb1d6f2a35b4fd099a25391909e05c2f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:22:52 -0500 Subject: [PATCH 123/131] [ML] Add Anomaly charts embeddables to Dashboard from Anomaly Explorer page (#95623) Co-authored-by: Robert Oskamp Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/index.ts | 3 +- x-pack/plugins/ml/common/types/fields.ts | 15 +- .../ml/common/util/runtime_field_utils.ts | 6 +- .../components/data_grid/common.ts | 5 +- .../form_options_validation.ts | 4 +- .../hooks/use_index_data.ts | 6 +- .../explorer/actions/load_explorer_data.ts | 115 +++---- .../explorer/add_to_dashboard_control.tsx | 312 ------------------ .../explorer/anomaly_context_menu.tsx | 110 ++++++ .../application/explorer/anomaly_timeline.tsx | 4 +- ...d_anomaly_charts_to_dashboard_controls.tsx | 128 +++++++ .../add_swimlane_to_dashboard_controls.tsx | 159 +++++++++ .../add_to_dashboard_controls.tsx | 123 +++++++ .../use_add_to_dashboard_actions.tsx | 70 ++++ .../use_dashboards_table.tsx | 82 +++++ .../public/application/explorer/explorer.js | 33 +- .../explorer/explorer_constants.ts | 4 +- .../explorer/explorer_dashboard_service.ts | 6 + .../application/explorer/explorer_utils.d.ts | 4 +- .../application/explorer/explorer_utils.js | 59 ---- .../reducers/explorer_reducer/reducer.ts | 14 +- .../reducers/explorer_reducer/state.ts | 2 + .../application/routing/routes/explorer.tsx | 8 + .../anomaly_explorer_charts_service.ts | 1 + .../anomaly_explorer_charts_service.test.ts | 29 -- .../anomaly_explorer_charts_service.ts | 59 +++- .../results_service/result_service_rx.ts | 134 +++++++- .../results_service/results_service.d.ts | 1 - .../results_service/results_service.js | 141 -------- .../anomaly_charts_initializer.tsx | 4 +- .../use_anomaly_charts_input_resolver.test.ts | 71 ++-- .../use_anomaly_charts_input_resolver.ts | 10 +- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 34 files changed, 1026 insertions(+), 704 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index c15aa8f414fb1..a64a0c0ae09fe 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -10,6 +10,7 @@ export { ChartData } from './types/field_histograms'; export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { isPopulatedObject } from './util/object_utils'; -export { isRuntimeMappings } from './util/runtime_field_utils'; export { composeValidators, patternValidator } from './util/validators'; +export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; export { extractErrorMessage } from './util/errors'; +export type { RuntimeMappings } from './types/fields'; diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 8dfe9d111ed38..45fcfac7e930c 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -28,7 +28,7 @@ export interface Field { aggregatable?: boolean; aggIds?: AggId[]; aggs?: Aggregation[]; - runtimeField?: RuntimeField; + runtimeField?: estypes.RuntimeField; } export interface Aggregation { @@ -108,17 +108,4 @@ export interface AggCardinality { export type RollupFields = Record]>; -// Replace this with import once #88995 is merged -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; - -export interface RuntimeField { - type: RuntimeType; - script?: - | string - | { - source: string; - }; -} - export type RuntimeMappings = estypes.RuntimeFields; diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.ts index 6d911ecd5d3cb..7be2a3ec8c9e1 100644 --- a/x-pack/plugins/ml/common/util/runtime_field_utils.ts +++ b/x-pack/plugins/ml/common/util/runtime_field_utils.ts @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { estypes } from '@elastic/elasticsearch'; import { isPopulatedObject } from './object_utils'; import { RUNTIME_FIELD_TYPES } from '../../../../../src/plugins/data/common'; -import type { RuntimeField, RuntimeMappings } from '../types/fields'; +import type { RuntimeMappings } from '../types/fields'; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; -export function isRuntimeField(arg: unknown): arg is RuntimeField { +export function isRuntimeField(arg: unknown): arg is estypes.RuntimeField { return ( ((isPopulatedObject(arg, ['type']) && Object.keys(arg).length === 1) || (isPopulatedObject(arg, ['type', 'script']) && diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index d3e58c4d7bb0d..f723c1d72b818 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from 'src/core/public'; +import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern, IFieldType, @@ -49,7 +50,7 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -import { RuntimeMappings, RuntimeField } from '../../../../common/types/fields'; +import { RuntimeMappings } from '../../../../common/types/fields'; import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; export const INIT_MAX_COLUMNS = 10; @@ -179,7 +180,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results export const NON_AGGREGATABLE = 'non-aggregatable'; export const getDataGridSchemaFromESFieldType = ( - fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type'] + fieldType: ES_FIELD_TYPES | undefined | estypes.RuntimeField['type'] ): string | undefined => { // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts index 1e1f376049579..79986e8ddb098 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { RuntimeType } from '../../../../../../../../../../src/plugins/data/common'; import { EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; @@ -18,7 +18,7 @@ export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']); // Regression supports numeric fields. Classification supports categorical, numeric, and boolean. export const shouldAddAsDepVarOption = ( fieldId: string, - fieldType: ES_FIELD_TYPES | RuntimeType, + fieldType: ES_FIELD_TYPES | estypes.RuntimeField['type'], jobType: AnalyticsJobType ) => { if (fieldId === EVENT_RATE_FIELD_ID) return false; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index f48f4a62f5a7d..2d9ae1cd4689b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -13,7 +13,7 @@ import { CoreSetup } from 'src/core/public'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; -import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields'; +import { RuntimeMappings } from '../../../../../../common/types/fields'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; @@ -44,7 +44,7 @@ interface MLEuiDataGridColumn extends EuiDataGridColumn { function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { return Object.keys(runtimeMappings).map((id) => { const field = runtimeMappings[id]; - const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']); + const schema = getDataGridSchemaFromESFieldType(field.type as estypes.RuntimeField['type']); return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true }; }); } @@ -64,7 +64,7 @@ export const useIndexData = ( const field = indexPattern.fields.getByName(id); const isRuntimeFieldColumn = field?.runtimeField !== undefined; const schema = isRuntimeFieldColumn - ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type']) + ? getDataGridSchemaFromESFieldType(field?.type as estypes.RuntimeField['type']) : getDataGridSchemaFromKibanaFieldType(field); return { id, diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index e09e9f3d2c1ae..1871e8925cb75 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -10,7 +10,7 @@ import { isEqual } from 'lodash'; import useObservable from 'react-use/lib/useObservable'; import { forkJoin, of, Observable, Subject } from 'rxjs'; -import { mergeMap, switchMap, tap } from 'rxjs/operators'; +import { mergeMap, switchMap, tap, map } from 'rxjs/operators'; import { useCallback, useMemo } from 'react'; import { explorerService } from '../explorer_dashboard_service'; @@ -21,7 +21,6 @@ import { getSelectionTimeRange, loadAnnotationsTableData, loadAnomaliesTableData, - loadDataForCharts, loadFilteredTopInfluencers, loadTopInfluencers, AppStateSelectedCells, @@ -36,8 +35,9 @@ import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants'; import { TimefilterContract } from '../../../../../../../src/plugins/data/public'; import { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service'; import { CombinedJob } from '../../../../common/types/anomaly_detection_jobs'; -import { mlJobService } from '../../services/job_service'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; +import { ExplorerChartsData } from '../explorer_charts/explorer_charts_container_service'; +import { mlJobService } from '../../services/job_service'; // Memoize the data fetching methods. // wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument @@ -58,7 +58,6 @@ const memoize = any>(func: T, context?: any) => { const memoizedLoadAnnotationsTableData = memoize( loadAnnotationsTableData ); -const memoizedLoadDataForCharts = memoize(loadDataForCharts); const memoizedLoadFilteredTopInfluencers = memoize( loadFilteredTopInfluencers ); @@ -96,7 +95,7 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi const loadExplorerDataProvider = ( mlResultsService: MlResultsService, anomalyTimelineService: AnomalyTimelineService, - anomalyExplorerService: AnomalyExplorerChartsService, + anomalyExplorerChartsService: AnomalyExplorerChartsService, timefilter: TimefilterContract ) => { const memoizedLoadOverallData = memoize( @@ -108,8 +107,8 @@ const loadExplorerDataProvider = ( anomalyTimelineService ); const memoizedAnomalyDataChange = memoize( - anomalyExplorerService.getAnomalyData, - anomalyExplorerService + anomalyExplorerChartsService.getAnomalyData, + anomalyExplorerChartsService ); return (config: LoadExplorerDataConfig): Observable> => { @@ -160,9 +159,7 @@ const loadExplorerDataProvider = ( swimlaneBucketInterval.asSeconds(), bounds ), - anomalyChartRecords: memoizedLoadDataForCharts( - lastRefresh, - mlResultsService, + anomalyChartRecords: anomalyExplorerChartsService.loadDataForCharts$( jobIds, timerange.earliestMs, timerange.latestMs, @@ -214,42 +211,30 @@ const loadExplorerDataProvider = ( // show the view-by loading indicator // and pass on the data we already fetched. tap(explorerService.setViewBySwimlaneLoading), - // Trigger a side-effect to update the charts. - tap(({ anomalyChartRecords, topFieldValues }) => { - if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - anomalyChartRecords, - timerange.earliestMs, - timerange.latestMs, - timefilter, - tableSeverity - ); - } else { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - [], - timerange.earliestMs, - timerange.latestMs, - timefilter, - tableSeverity - ); - } - }), - // Load view-by swimlane data and filtered top influencers. - // mergeMap is used to have access to the already fetched data and act on it in arg #1. - // In arg #2 of mergeMap we combine the data and pass it on in the action format - // which can be consumed by explorerReducer() later on. + tap(explorerService.setChartsDataLoading), mergeMap( - ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => + ({ + anomalyChartRecords, + influencers, + overallState, + topFieldValues, + annotationsData, + tableData, + }) => forkJoin({ - influencers: + anomalyChartsData: memoizedAnomalyDataChange( + lastRefresh, + combinedJobRecords, + swimlaneContainerWidth, + selectedCells !== undefined && Array.isArray(anomalyChartRecords) + ? anomalyChartRecords + : [], + timerange.earliestMs, + timerange.latestMs, + timefilter, + tableSeverity + ), + filteredTopInfluencers: (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && anomalyChartRecords !== undefined && anomalyChartRecords.length > 0 @@ -280,24 +265,26 @@ const loadExplorerDataProvider = ( swimlaneContainerWidth, influencersFilterQuery ), - }), - ( - { annotationsData, overallState, tableData }, - { influencers, viewBySwimlaneState } - ): Partial => { - return { - annotations: annotationsData, - influencers: influencers as any, - loading: false, - viewBySwimlaneDataLoading: false, - overallSwimlaneData: overallState, - viewBySwimlaneData: viewBySwimlaneState as any, - tableData, - swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) - ? viewBySwimlaneState.cardinality - : undefined, - }; - } + }).pipe( + tap(({ anomalyChartsData }) => { + explorerService.setCharts(anomalyChartsData as ExplorerChartsData); + }), + map(({ viewBySwimlaneState, filteredTopInfluencers }) => { + return { + annotations: annotationsData, + influencers: filteredTopInfluencers as any, + loading: false, + viewBySwimlaneDataLoading: false, + anomalyChartsDataLoading: false, + overallSwimlaneData: overallState, + viewBySwimlaneData: viewBySwimlaneState as any, + tableData, + swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) + ? viewBySwimlaneState.cardinality + : undefined, + }; + }) + ) ) ); }; @@ -319,7 +306,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) uiSettings, mlResultsService ); - const anomalyExplorerService = new AnomalyExplorerChartsService( + const anomalyExplorerChartsService = new AnomalyExplorerChartsService( timefilter, mlApiServices, mlResultsService @@ -327,7 +314,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) return loadExplorerDataProvider( mlResultsService, anomalyTimelineService, - anomalyExplorerService, + anomalyExplorerChartsService, timefilter ); }, []); diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx deleted file mode 100644 index 8fe2c32b766b4..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC, useCallback, useMemo, useState, useEffect } from 'react'; -import { debounce } from 'lodash'; -import { - EuiFormRow, - EuiCheckboxGroup, - EuiInMemoryTableProps, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiButtonEmpty, - EuiButton, - EuiModalFooter, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiModalBody } from '@elastic/eui'; -import { EuiInMemoryTable } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../contexts/kibana'; -import { DashboardSavedObject } from '../../../../../../src/plugins/dashboard/public'; -import { getDefaultSwimlanePanelTitle } from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; -import { useDashboardService } from '../services/dashboard_service'; -import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; -import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../embeddables'; - -export interface DashboardItem { - id: string; - title: string; - description: string | undefined; - attributes: DashboardSavedObject; -} - -export type EuiTableProps = EuiInMemoryTableProps; - -function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { - return { - type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - title: getDefaultSwimlanePanelTitle(jobIds), - }; -} - -interface AddToDashboardControlProps { - jobIds: JobId[]; - viewBy: string; - onClose: (callback?: () => Promise) => void; -} - -/** - * Component for attaching anomaly swim lane embeddable to dashboards. - */ -export const AddToDashboardControl: FC = ({ - onClose, - jobIds, - viewBy, -}) => { - const { - notifications: { toasts }, - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); - - useEffect(() => { - fetchDashboards(); - - return () => { - fetchDashboards.cancel(); - }; - }, []); - - const dashboardService = useDashboardService(); - - const [isLoading, setIsLoading] = useState(false); - const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ - [SWIMLANE_TYPE.OVERALL]: true, - [SWIMLANE_TYPE.VIEW_BY]: false, - }); - const [dashboardItems, setDashboardItems] = useState([]); - const [selectedItems, setSelectedItems] = useState([]); - - const fetchDashboards = useCallback( - debounce(async (query?: string) => { - try { - const response = await dashboardService.fetchDashboards(query); - const items: DashboardItem[] = response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - attributes: savedObject.attributes, - }; - }); - setDashboardItems(items); - } catch (e) { - toasts.danger({ - body: e, - }); - } - setIsLoading(false); - }, 500), - [] - ); - - const search: EuiTableProps['search'] = useMemo(() => { - return { - onChange: ({ queryText }) => { - setIsLoading(true); - fetchDashboards(queryText); - }, - box: { - incremental: true, - 'data-test-subj': 'mlDashboardsSearchBox', - }, - }; - }, []); - - const addSwimlaneToDashboardCallback = useCallback(async () => { - const swimlanes = Object.entries(selectedSwimlanes) - .filter(([, isSelected]) => isSelected) - .map(([swimlaneType]) => swimlaneType); - - for (const selectedDashboard of selectedItems) { - const panelsData = swimlanes.map((swimlaneType) => { - const config = getDefaultEmbeddablePanelConfig(jobIds); - if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - viewBy, - }, - }; - } - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - }, - }; - }); - - try { - await dashboardService.attachPanels( - selectedDashboard.id, - selectedDashboard.attributes, - panelsData - ); - toasts.success({ - title: ( - - ), - toastLifeTimeMs: 3000, - }); - } catch (e) { - toasts.danger({ - body: e, - }); - } - } - }, [selectedSwimlanes, selectedItems]); - - const columns: EuiTableProps['columns'] = [ - { - field: 'title', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { - defaultMessage: 'Title', - }), - sortable: true, - truncateText: true, - }, - { - field: 'description', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { - defaultMessage: 'Description', - }), - truncateText: true, - }, - ]; - - const swimlaneTypeOptions = [ - { - id: SWIMLANE_TYPE.OVERALL, - label: i18n.translate('xpack.ml.explorer.overallLabel', { - defaultMessage: 'Overall', - }), - }, - { - id: SWIMLANE_TYPE.VIEW_BY, - label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { - defaultMessage: 'View by {viewByField}', - values: { viewByField: viewBy }, - }), - }, - ]; - - const selection: EuiTableProps['selection'] = { - onSelectionChange: setSelectedItems, - }; - - const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); - - return ( - - - - - - - - - } - > - { - const newSelection = { - ...selectedSwimlanes, - [optionId]: !selectedSwimlanes[optionId as SwimlaneType], - }; - setSelectedSwimlanes(newSelection); - }} - data-test-subj="mlAddToDashboardSwimlaneTypeSelector" - /> - - - - - - } - data-test-subj="mlDashboardSelectionContainer" - > - - - - - - - - { - onClose(async () => { - const selectedDashboardId = selectedItems[0].id; - await addSwimlaneToDashboardCallback(); - await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); - }); - }} - data-test-subj="mlAddAndEditDashboardButton" - > - - - - - - - - ); -}; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx new file mode 100644 index 0000000000000..9f65449169ee6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, FC } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../contexts/kibana'; +import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils'; +import { TimeRangeBounds } from '../util/time_buckets'; +import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls'; + +interface AnomalyContextMenuProps { + selectedJobs: ExplorerJob[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + chartsCount: number; +} +export const AnomalyContextMenu: FC = ({ + selectedJobs, + selectedCells, + bounds, + interval, + chartsCount, +}) => { + const { + services: { + application: { capabilities }, + }, + } = useMlKibana(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false); + + const canEditDashboards = capabilities.dashboard?.createNew ?? false; + const menuItems = useMemo(() => { + const items = []; + if (canEditDashboards) { + items.push( + + + + ); + } + return items; + }, [canEditDashboards]); + + const jobIds = selectedJobs.map(({ id }) => id); + + return ( + <> + {menuItems.length > 0 && ( + + + } + isOpen={isMenuOpen} + closePopover={setIsMenuOpen.bind(null, false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + )} + {isAddDashboardsActive && selectedJobs && ( + { + setIsAddDashboardActive(false); + if (callback) { + await callback(); + } + }} + selectedCells={selectedCells} + bounds={bounds} + interval={interval} + jobIds={jobIds} + /> + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 7c63d4087ce1e..37967d18dbbd9 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -24,7 +24,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; -import { AddToDashboardControl } from './add_to_dashboard_control'; +import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls'; import { useMlKibana } from '../contexts/kibana'; import { TimeBuckets } from '../util/time_buckets'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; @@ -294,7 +294,7 @@ export const AnomalyTimeline: FC = React.memo( )} {isAddDashboardsActive && selectedJobs && ( - { setIsAddDashboardActive(false); if (callback) { diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..5c3c6edee59c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldNumber, EuiFormRow, formatDate } from '@elastic/eui'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; +import { AppStateSelectedCells, getSelectionTimeRange } from '../explorer_utils'; +import { TimeRange } from '../../../../../../../src/plugins/data/common/query'; +import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../services/anomaly_explorer_charts_service'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { getDefaultExplorerChartsPanelTitle } from '../../../embeddables/anomaly_charts/anomaly_charts_embeddable'; +import { TimeRangeBounds } from '../../util/time_buckets'; +import { useTableSeverity } from '../../components/controls/select_severity'; +import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../../embeddables/anomaly_charts/anomaly_charts_initializer'; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + title: getDefaultExplorerChartsPanelTitle(jobIds), + }; +} + +export interface AddToDashboardControlProps { + jobIds: string[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddAnomalyChartsToDashboardControl: FC = ({ + onClose, + jobIds, + selectedCells, + bounds, + interval, +}) => { + const [severity] = useTableSeverity(); + const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT); + + const getPanelsData = useCallback(async () => { + let timeRange: TimeRange | undefined; + if (selectedCells !== undefined && interval !== undefined && bounds !== undefined) { + const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, interval, bounds); + timeRange = { + from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + mode: 'absolute', + }; + } + + const config = getDefaultEmbeddablePanelConfig(jobIds); + return [ + { + ...config, + embeddableConfig: { + jobIds, + maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT, + severityThreshold: severity.val, + ...(timeRange ?? {}), + }, + }, + ]; + }, [selectedCells, interval, bounds, jobIds, maxSeriesToPlot, severity]); + + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + const title = ( + + ); + + const disabled = selectedItems.length < 1 && !Array.isArray(jobIds === undefined); + + const extraControls = ( + + } + > + setMaxSeriesToPlot(parseInt(e.target.value, 10))} + min={0} + max={MAX_ANOMALY_CHARTS_ALLOWED} + /> + + ); + + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..79089e7e5baf9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback, useState } from 'react'; +import { EuiFormRow, EuiCheckboxGroup, EuiInMemoryTableProps, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { getDefaultSwimlanePanelTitle } from '../../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; +import { SWIMLANE_TYPE, SwimlaneType } from '../explorer_constants'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + title: getDefaultSwimlanePanelTitle(jobIds), + }; +} + +interface AddToDashboardControlProps { + jobIds: JobId[]; + viewBy: string; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddSwimlaneToDashboardControl: FC = ({ + onClose, + jobIds, + viewBy, +}) => { + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + + const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ + [SWIMLANE_TYPE.OVERALL]: true, + [SWIMLANE_TYPE.VIEW_BY]: false, + }); + + const getPanelsData = useCallback(async () => { + const swimlanes = Object.entries(selectedSwimlanes) + .filter(([, isSelected]) => isSelected) + .map(([swimlaneType]) => swimlaneType); + + return swimlanes.map((swimlaneType) => { + const config = getDefaultEmbeddablePanelConfig(jobIds); + if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + viewBy, + }, + }; + } + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + }, + }; + }); + }, [selectedSwimlanes, selectedItems]); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + + const swimlaneTypeOptions = [ + { + id: SWIMLANE_TYPE.OVERALL, + label: i18n.translate('xpack.ml.explorer.overallLabel', { + defaultMessage: 'Overall', + }), + }, + { + id: SWIMLANE_TYPE.VIEW_BY, + label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { + defaultMessage: 'View by {viewByField}', + values: { viewByField: viewBy }, + }), + }, + ]; + + const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); + + const extraControls = ( + <> + + } + > + { + const newSelection = { + ...selectedSwimlanes, + [optionId]: !selectedSwimlanes[optionId as SwimlaneType], + }; + setSelectedSwimlanes(newSelection); + }} + data-test-subj="mlAddToDashboardSwimlaneTypeSelector" + /> + + + + ); + + const title = ( + + ); + + const disabled = noSwimlaneSelected || selectedItems.length === 0; + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..7806e531834a1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFormRow, + EuiInMemoryTable, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTableProps, useDashboardTable } from './use_dashboards_table'; + +export const columns: EuiTableProps['columns'] = [ + { + field: 'title', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { + defaultMessage: 'Title', + }), + sortable: true, + truncateText: true, + }, + { + field: 'description', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { + defaultMessage: 'Description', + }), + truncateText: true, + }, +]; + +interface AddToDashboardControlProps extends ReturnType { + onClose: (callback?: () => Promise) => void; + addToDashboardAndEditCallback: () => Promise; + addToDashboardCallback: () => Promise; + title: React.ReactNode; + disabled: boolean; + children?: React.ReactElement; +} +export const AddToDashboardControl: FC = ({ + onClose, + selection, + dashboardItems, + isLoading, + search, + addToDashboardAndEditCallback, + addToDashboardCallback, + title, + disabled, + children, +}) => { + return ( + + + {title} + + + {children} + + } + data-test-subj="mlDashboardSelectionContainer" + > + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx new file mode 100644 index 0000000000000..82c699865f2e4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DashboardItem } from './use_dashboards_table'; +import { SavedDashboardPanel } from '../../../../../../../src/plugins/dashboard/common/types'; +import { useMlKibana } from '../../contexts/kibana'; +import { useDashboardService } from '../../services/dashboard_service'; + +export const useAddToDashboardActions = ({ + onClose, + getPanelsData, + selectedDashboards, +}: { + onClose: (callback?: () => Promise) => void; + getPanelsData: ( + selectedDashboards: DashboardItem[] + ) => Promise>>; + selectedDashboards: DashboardItem[]; +}) => { + const { + notifications: { toasts }, + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + const dashboardService = useDashboardService(); + + const addToDashboardCallback = useCallback(async () => { + const panelsData = await getPanelsData(selectedDashboards); + for (const selectedDashboard of selectedDashboards) { + try { + await dashboardService.attachPanels( + selectedDashboard.id, + selectedDashboard.attributes, + panelsData + ); + toasts.success({ + title: ( + + ), + toastLifeTimeMs: 3000, + }); + } catch (e) { + toasts.danger({ + body: e, + }); + } + } + }, [selectedDashboards, getPanelsData]); + + const addToDashboardAndEditCallback = useCallback(async () => { + onClose(async () => { + await addToDashboardCallback(); + const selectedDashboardId = selectedDashboards[0].id; + await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); + }); + }, [addToDashboardCallback, selectedDashboards, navigateToUrl]); + + return { addToDashboardCallback, addToDashboardAndEditCallback }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx new file mode 100644 index 0000000000000..8721de497eedc --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiInMemoryTableProps } from '@elastic/eui'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; +import type { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { useDashboardService } from '../../services/dashboard_service'; +import { useMlKibana } from '../../contexts/kibana'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +export const useDashboardTable = () => { + const { + notifications: { toasts }, + } = useMlKibana(); + + const dashboardService = useDashboardService(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchDashboards(); + + return () => { + fetchDashboards.cancel(); + }; + }, []); + + const search: EuiTableProps['search'] = useMemo(() => { + return { + onChange: ({ queryText }) => { + setIsLoading(true); + fetchDashboards(queryText); + }, + box: { + incremental: true, + 'data-test-subj': 'mlDashboardsSearchBox', + }, + }; + }, []); + + const [dashboardItems, setDashboardItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchDashboards = useCallback( + debounce(async (query?: string) => { + try { + const response = await dashboardService.fetchDashboards(query); + const items: DashboardItem[] = response.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + attributes: savedObject.attributes, + }; + }); + setDashboardItems(items); + } catch (e) { + toasts.danger({ + body: e, + }); + } + setIsLoading(false); + }, 500), + [] + ); + const selection: EuiTableProps['selection'] = { + onSelectionChange: setSelectedItems, + }; + return { dashboardItems, selectedItems, selection, search, isLoading }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 6979277c43077..45665b2026db5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -72,6 +72,7 @@ import { getToastNotifications } from '../util/dependency_cache'; import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; +import { AnomalyContextMenu } from './anomaly_context_menu'; const ExplorerPage = ({ children, @@ -431,14 +432,32 @@ export class ExplorerUI extends React.Component { )} {loading === false && ( - -

- + + +

+ +

+
+
+ + + -

-
+ + { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); + }, clearInfluencerFilterSettings: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS }); }, @@ -137,6 +140,9 @@ export const explorerService = { setFilterData: (payload: Partial>) => { explorerAction$.next(setFilterDataActionCreator(payload)); }, + setChartsDataLoading: () => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); + }, setSwimlaneContainerWidth: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts index 9e24a4349584e..b410449218d02 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -12,6 +12,7 @@ import { TimeRangeBounds } from '../util/time_buckets'; import { RecordForInfluencer } from '../services/results_service/results_service'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; import { MlResultsService } from '../services/results_service'; +import { EntityField } from '../../../common/util/anomaly_utils'; interface ClearedSelectedAnomaliesState { selectedCells: undefined; @@ -60,7 +61,7 @@ export declare const getSelectionJobIds: ( export declare const getSelectionInfluencers: ( selectedCells: AppStateSelectedCells | undefined, fieldName: string -) => string[]; +) => EntityField[]; interface SelectionTimeRange { earliestMs: number; @@ -149,6 +150,7 @@ export declare const loadDataForCharts: ( ) => Promise; export declare const loadFilteredTopInfluencers: ( + mlResultsService: MlResultsService, jobIds: string[], earliestMs: number, latestMs: number, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index ea101d104f783..69bdac060a2dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -536,65 +536,6 @@ export async function loadAnomaliesTableData( }); } -// track the request to be able to ignore out of date requests -// and avoid race conditions ending up with the wrong charts. -let requestCount = 0; -export async function loadDataForCharts( - mlResultsService, - jobIds, - earliestMs, - latestMs, - influencers = [], - selectedCells, - influencersFilterQuery, - // choose whether or not to keep track of the request that could be out of date - // in Anomaly Explorer this is being used to ignore any request that are out of date - // but in embeddables, we might have multiple requests coming from multiple different panels - takeLatestOnly = true -) { - return new Promise((resolve) => { - // Just skip doing the request when this function - // is called without the minimum required data. - if ( - selectedCells === undefined && - influencers.length === 0 && - influencersFilterQuery === undefined - ) { - resolve([]); - } - - const newRequestCount = ++requestCount; - requestCount = newRequestCount; - - // Load the top anomalies (by record_score) which will be displayed in the charts. - mlResultsService - .getRecordsForInfluencer( - jobIds, - influencers, - 0, - earliestMs, - latestMs, - 500, - influencersFilterQuery - ) - .then((resp) => { - // Ignore this response if it's returned by an out of date promise - if (takeLatestOnly && newRequestCount < requestCount) { - resolve([]); - } - - if ( - (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || - influencersFilterQuery !== undefined - ) { - resolve(resp.records); - } - - resolve([]); - }); - }); -} - export async function loadTopInfluencers( mlResultsService, selectedJobIds, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index f66cd94314608..15e0caa29af39 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -20,7 +20,7 @@ import { import { checkSelectedCells } from './check_selected_cells'; import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; import { jobSelectionChange } from './job_selection_change'; -import { ExplorerState } from './state'; +import { ExplorerState, getExplorerDefaultState } from './state'; import { setInfluencerFilterSettings } from './set_influencer_filter_settings'; import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; import { getTimeBoundsFromSelection } from '../../hooks/use_selected_cells'; @@ -31,6 +31,10 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo let nextState: ExplorerState; switch (type) { + case EXPLORER_ACTION.CLEAR_EXPLORER_DATA: + nextState = getExplorerDefaultState(); + break; + case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: nextState = clearInfluencerFilterSettings(state); break; @@ -49,6 +53,14 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo nextState = jobSelectionChange(state, payload); break; + case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING: + nextState = { + ...state, + anomalyChartsDataLoading: true, + chartsData: getDefaultChartsData(), + }; + break; + case EXPLORER_ACTION.SET_CHARTS: nextState = { ...state, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index bb90fedfc2315..e9527b7c232e5 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -28,6 +28,7 @@ import { InfluencersFilterQuery } from '../../../../../common/types/es_client'; export interface ExplorerState { annotations: AnnotationsTable; + anomalyChartsDataLoading: boolean; chartsData: ExplorerChartsData; fieldFormatsLoading: boolean; filterActive: boolean; @@ -69,6 +70,7 @@ export function getExplorerDefaultState(): ExplorerState { annotationsData: [], aggregations: {}, }, + anomalyChartsDataLoading: true, chartsData: getDefaultChartsData(), fieldFormatsLoading: false, filterActive: false, diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index b651b311f13aa..3e5cf252230a2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -159,6 +159,14 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim } }, [JSON.stringify(jobIds)]); + useEffect(() => { + return () => { + // upon component unmounting + // clear any data to prevent next page from rendering old charts + explorerService.clearExplorerData(); + }; + }, []); + /** * TODO get rid of the intermediate state in explorerService. * URL state should be the only source of truth for related props. diff --git a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts index 21f07ed9e5a3c..28140038d249b 100644 --- a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts @@ -10,4 +10,5 @@ export const createAnomalyExplorerChartsServiceMock = () => ({ getAnomalyData: jest.fn(), setTimeRange: jest.fn(), getTimeBounds: jest.fn(), + loadDataForCharts$: jest.fn(), }); diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts index 36e18b49cfa84..ac61e11b1128e 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts @@ -13,7 +13,6 @@ import { of } from 'rxjs'; import { cloneDeep } from 'lodash'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { ExplorerChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import type { MlApiServices } from './ml_api_service'; import type { MlResultsService } from './results_service'; import { getDefaultChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; @@ -89,9 +88,6 @@ describe('AnomalyExplorerChartsService', () => { (mlApiServicesMock as unknown) as MlApiServices, (mlResultsServiceMock as unknown) as MlResultsService ); - const explorerService = { - setCharts: jest.fn(), - }; const timeRange = { earliestMs: 1486656000000, @@ -104,13 +100,8 @@ describe('AnomalyExplorerChartsService', () => { ); }); - afterEach(() => { - explorerService.setCharts.mockClear(); - }); - test('should return anomaly data without explorer service', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecords, @@ -123,27 +114,8 @@ describe('AnomalyExplorerChartsService', () => { assertAnomalyDataResult(anomalyData); }); - test('should set anomaly data with explorer service side effects', async () => { - await anomalyExplorerService.getAnomalyData( - (explorerService as unknown) as ExplorerService, - (combinedJobRecords as unknown) as Record, - 1000, - mockAnomalyChartRecords, - timeRange.earliestMs, - timeRange.latestMs, - timefilterMock, - 0, - 12 - ); - - expect(explorerService.setCharts.mock.calls.length).toBe(2); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[0][0]); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[1][0]); - }); - test('call anomalyChangeListener with empty series config', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, // @ts-ignore (combinedJobRecords as unknown) as Record, 1000, @@ -165,7 +137,6 @@ describe('AnomalyExplorerChartsService', () => { mockAnomalyChartRecordsClone[1].partition_field_value = 'AAL.'; const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecordsClone, diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 72de5d003d4b8..7aff2ff7e0026 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -7,6 +7,8 @@ import { each, find, get, map, reduce, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Observable, of } from 'rxjs'; +import { map as mapObservable } from 'rxjs/operators'; import { RecordForInfluencer } from './results_service/results_service'; import { isMappableJob, @@ -29,7 +31,6 @@ import { CHART_TYPE, ChartType } from '../explorer/explorer_constants'; import type { ChartRecord } from '../explorer/explorer_utils'; import { RecordsForCriteria, ScheduledEventsByBucket } from './results_service/result_service_rx'; import { isPopulatedObject } from '../../../common/util/object_utils'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { ExplorerChartsData, @@ -37,6 +38,8 @@ import { } from '../explorer/explorer_charts/explorer_charts_container_service'; import { TimeRangeBounds } from '../util/time_buckets'; import { isDefined } from '../../../common/types/guards'; +import { AppStateSelectedCells } from '../explorer/explorer_utils'; +import { InfluencersFilterQuery } from '../../../common/types/es_client'; const CHART_MAX_POINTS = 500; const ANOMALIES_MAX_RESULTS = 500; const MAX_SCHEDULED_EVENTS = 10; // Max number of scheduled events displayed per bucket. @@ -370,15 +373,53 @@ export class AnomalyExplorerChartsService { // Getting only necessary job config and datafeed config without the stats jobIds.map((jobId) => this.mlApiServices.jobs.jobForCloning(jobId)) ); - const combinedJobs = combinedResults + return combinedResults .filter(isDefined) .filter((r) => r.job !== undefined && r.datafeed !== undefined) .map(({ job, datafeed }) => ({ ...job, datafeed_config: datafeed } as CombinedJob)); - return combinedJobs; + } + + public loadDataForCharts$( + jobIds: string[], + earliestMs: number, + latestMs: number, + influencers: EntityField[] = [], + selectedCells: AppStateSelectedCells | undefined, + influencersFilterQuery: InfluencersFilterQuery + ): Observable { + if ( + selectedCells === undefined && + influencers.length === 0 && + influencersFilterQuery === undefined + ) { + of([]); + } + + return this.mlResultsService + .getRecordsForInfluencer$( + jobIds, + influencers, + 0, + earliestMs, + latestMs, + 500, + influencersFilterQuery + ) + .pipe( + mapObservable((resp): RecordForInfluencer[] => { + if ( + (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || + influencersFilterQuery !== undefined + ) { + return resp.records; + } + + return [] as RecordForInfluencer[]; + }) + ); } public async getAnomalyData( - explorerService: ExplorerService | undefined, combinedJobRecords: Record, chartsContainerWidth: number, anomalyRecords: ChartRecord[] | undefined, @@ -486,9 +527,6 @@ export class AnomalyExplorerChartsService { data.errorMessages = errorMessages; } - if (explorerService) { - explorerService.setCharts({ ...data }); - } if (seriesConfigs.length === 0) { return data; } @@ -848,9 +886,6 @@ export class AnomalyExplorerChartsService { // push map data in if it's available data.seriesToPlot.push(...mapData); } - if (explorerService) { - explorerService.setCharts({ ...data }); - } return Promise.resolve(data); }) .catch((error) => { @@ -860,7 +895,7 @@ export class AnomalyExplorerChartsService { } public processRecordsForDisplay( - jobRecords: Record, + combinedJobRecords: Record, anomalyRecords: RecordForInfluencer[] ): { records: ChartRecord[]; errors: Record> | undefined } { // Aggregate the anomaly data by detector, and entity (by/over/partition). @@ -875,7 +910,7 @@ export class AnomalyExplorerChartsService { // Check if we can plot a chart for this record, depending on whether the source data // is chartable, and if model plot is enabled for the job. - const job = jobRecords[record.job_id]; + const job = combinedJobRecords[record.job_id]; // if we already know this job has datafeed aggregations we cannot support // no need to do more checks diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index e07d49ca23d3b..caa0e20c3230d 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -22,9 +22,11 @@ import { MlApiServices } from '../ml_api_service'; import { CriteriaField } from './index'; import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; -import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; +import { aggregationTypeTransform, EntityField } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { isPopulatedObject } from '../../../../common/util/object_utils'; +import { InfluencersFilterQuery } from '../../../../common/types/es_client'; +import { RecordForInfluencer } from './results_service'; interface ResultResponse { success: boolean; @@ -633,5 +635,135 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { latestMs ); }, + + // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), + // for the specified job(s), time range, and record score threshold. + // influencers parameter must be an array, with each object in the array having 'fieldName' + // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, + // so this returns record level results which have at least one of the influencers. + // Pass an empty array or ['*'] to search over all job IDs. + getRecordsForInfluencer$( + jobIds: string[], + influencers: EntityField[], + threshold: number, + earliestMs: number, + latestMs: number, + maxResults: number, + influencersFilterQuery: InfluencersFilterQuery + ): Observable<{ records: RecordForInfluencer[]; success: boolean }> { + const obj = { success: true, records: [] as RecordForInfluencer[] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + // Add a nested query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': influencer.fieldName, + }, + }, + { + match: { + 'influencers.influencer_field_values': influencer.fieldValue, + }, + }, + ], + }, + }, + }, + }; + }), + minimum_should_match: 1, + }, + }); + } + + return mlApiServices.results + .anomalySearch$( + { + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }, + jobIds + ) + .pipe( + map((resp) => { + if (resp.hits.total.value > 0) { + each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); + }, }; } diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index d26e650d145cb..6161eeb4e7940 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -55,7 +55,6 @@ export function resultsServiceProvider( influencersFilterQuery: InfluencersFilterQuery ): Promise; getRecordInfluencers(): Promise; - getRecordsForInfluencer(): Promise; getRecordsForDetector(): Promise; getRecords(): Promise; getEventRateData( diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index b041267f46c04..c258d07cab484 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -779,139 +779,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), - // for the specified job(s), time range, and record score threshold. - // influencers parameter must be an array, with each object in the array having 'fieldName' - // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, - // so this returns record level results which have at least one of the influencers. - // Pass an empty array or ['*'] to search over all job IDs. - getRecordsForInfluencer( - jobIds, - influencers, - threshold, - earliestMs, - latestMs, - maxResults, - influencersFilterQuery - ) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - { - range: { - record_score: { - gte: threshold, - }, - }, - }, - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr, - }, - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - // Add a nested query to filter for each of the specified influencers. - if (influencers.length > 0) { - boolCriteria.push({ - bool: { - should: influencers.map((influencer) => { - return { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': influencer.fieldName, - }, - }, - { - match: { - 'influencers.influencer_field_values': influencer.fieldValue, - }, - }, - ], - }, - }, - }, - }; - }), - minimum_should_match: 1, - }, - }); - } - - mlApiServices.results - .anomalySearch( - { - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false, - }, - }, - { - bool: { - must: boolCriteria, - }, - }, - ], - }, - }, - sort: [{ record_score: { order: 'desc' } }], - }, - }, - jobIds - ) - .then((resp) => { - if (resp.hits.total.value > 0) { - each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); - }, - // Queries Elasticsearch to obtain the record level results for the specified job and detector, // time range, record score threshold, and whether to only return results containing influencers. // An additional, optional influencer field name and value may also be provided. @@ -1039,14 +906,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain all the record level results for the specified job(s), time range, - // and record score threshold. - // Pass an empty array or ['*'] to search over all job IDs. - // Returned response contains a records property, which is an array of the matching results. - getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { - return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); - }, - // Queries Elasticsearch to obtain event rate data i.e. the count // of documents over time. // index can be a String, or String[], of index names to search. diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx index f32446fd6d9ab..a36d063737704 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyChartsEmbeddableInput } from '..'; import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../application/services/anomaly_explorer_charts_service'; -const MAX_SERIES_ALLOWED = 48; +export const MAX_ANOMALY_CHARTS_ALLOWED = 48; export interface AnomalyChartsInitializerProps { defaultTitle: string; initialInput?: Partial>; @@ -98,7 +98,7 @@ export const AnomalyChartsInitializer: FC = ({ value={maxSeriesToPlot} onChange={(e) => setMaxSeriesToPlot(parseInt(e.target.value, 10))} min={0} - max={MAX_SERIES_ALLOWED} + max={MAX_ANOMALY_CHARTS_ALLOWED} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts index efac51edda69f..7045b2eac378a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts @@ -29,41 +29,6 @@ jest.mock('../../application/explorer/explorer_utils', () => ({ }), getSelectionJobIds: jest.fn(() => ['test-job']), getSelectionTimeRange: jest.fn(() => ({ earliestMs: 1521309543000, latestMs: 1616003942999 })), - loadDataForCharts: jest.fn().mockImplementation(() => - Promise.resolve([ - { - job_id: 'cw_multi_1', - result_type: 'record', - probability: 6.057139142746412e-13, - multi_bucket_impact: -5, - record_score: 89.71961, - initial_record_score: 98.36826274948001, - bucket_span: 900, - detector_index: 0, - is_interim: false, - timestamp: 1572892200000, - partition_field_name: 'instance', - partition_field_value: 'i-d17dcd4c', - function: 'mean', - function_description: 'mean', - typical: [1.6177685422858146], - actual: [7.235333333333333], - field_name: 'CPUUtilization', - influencers: [ - { - influencer_field_name: 'region', - influencer_field_values: ['sa-east-1'], - }, - { - influencer_field_name: 'instance', - influencer_field_values: ['i-d17dcd4c'], - }, - ], - instance: ['i-d17dcd4c'], - region: ['sa-east-1'], - }, - ]) - ), })); describe('useAnomalyChartsInputResolver', () => { @@ -115,6 +80,42 @@ describe('useAnomalyChartsInputResolver', () => { }) ); + anomalyExplorerChartsServiceMock.loadDataForCharts$.mockImplementation(() => + Promise.resolve([ + { + job_id: 'cw_multi_1', + result_type: 'record', + probability: 6.057139142746412e-13, + multi_bucket_impact: -5, + record_score: 89.71961, + initial_record_score: 98.36826274948001, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1572892200000, + partition_field_name: 'instance', + partition_field_value: 'i-d17dcd4c', + function: 'mean', + function_description: 'mean', + typical: [1.6177685422858146], + actual: [7.235333333333333], + field_name: 'CPUUtilization', + influencers: [ + { + influencer_field_name: 'region', + influencer_field_values: ['sa-east-1'], + }, + { + influencer_field_name: 'instance', + influencer_field_values: ['i-d17dcd4c'], + }, + ], + instance: ['i-d17dcd4c'], + region: ['sa-east-1'], + }, + ]) + ); + const coreStartMock = createCoreStartMock(); const mlStartMock = createMlStartDepsMock(); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts index b114ca89a3288..703851f3fe9b6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts @@ -18,7 +18,6 @@ import { getSelectionInfluencers, getSelectionJobIds, getSelectionTimeRange, - loadDataForCharts, } from '../../application/explorer/explorer_utils'; import { OVERALL_LABEL, SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; import { parseInterval } from '../../../common/util/parse_interval'; @@ -46,7 +45,7 @@ export function useAnomalyChartsInputResolver( const [ { uiSettings }, { data: dataServices }, - { anomalyDetectorService, anomalyExplorerService, mlResultsService }, + { anomalyDetectorService, anomalyExplorerService }, ] = services; const { timefilter } = dataServices.query.timefilter; @@ -125,15 +124,13 @@ export function useAnomalyChartsInputResolver( const timeRange = getSelectionTimeRange(selections, bucketInterval.asSeconds(), bounds); return forkJoin({ combinedJobs: anomalyExplorerService.getCombinedJobs(jobIds), - anomalyChartRecords: loadDataForCharts( - mlResultsService, + anomalyChartRecords: anomalyExplorerService.loadDataForCharts$( jobIds, timeRange.earliestMs, timeRange.latestMs, selectionInfluencers, selections, - influencersFilterQuery, - false + influencersFilterQuery ), }).pipe( switchMap(({ combinedJobs, anomalyChartRecords }) => { @@ -147,7 +144,6 @@ export function useAnomalyChartsInputResolver( return forkJoin({ chartsData: from( anomalyExplorerService.getAnomalyData( - undefined, combinedJobRecords, embeddableContainerWidth, anomalyChartRecords, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3f9be18e33d2a..24d5bd41b1ee4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13725,7 +13725,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "削除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "キャンセル", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "ダッシュボードを選択:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "スイムレーンビューを選択:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "スイムレーンをダッシュボードに追加", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "スイムレーンビューを選択:", "xpack.ml.explorer.addToDashboardLabel": "ダッシュボードに追加", "xpack.ml.explorer.annotationsErrorCallOutTitle": "注釈の読み込み中にエラーが発生しました。", "xpack.ml.explorer.annotationsErrorTitle": "注釈", @@ -13750,7 +13751,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "説明", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "ダッシュボード「{dashboardTitle}」は正常に更新されました", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "タイトル", - "xpack.ml.explorer.dashboardsTitle": "スイムレーンをダッシュボードに追加", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "異常スコア", "xpack.ml.explorer.distributionChart.entityLabel": "エンティティ", "xpack.ml.explorer.distributionChart.typicalLabel": "通常", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 25aa56d031fca..378b1bc1aa11a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13905,7 +13905,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "删除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "取消", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "选择仪表板:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "选择泳道视图:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "将泳道添加到仪表板", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "选择泳道视图:", "xpack.ml.explorer.addToDashboardLabel": "添加到仪表板", "xpack.ml.explorer.annotationsErrorCallOutTitle": "加载注释时发生错误:", "xpack.ml.explorer.annotationsErrorTitle": "标注", @@ -13930,7 +13931,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "描述", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "仪表板“{dashboardTitle}”已成功更新", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "标题", - "xpack.ml.explorer.dashboardsTitle": "将泳道添加到仪表板", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "异常分数", "xpack.ml.explorer.distributionChart.entityLabel": "实体", "xpack.ml.explorer.distributionChart.typicalLabel": "典型", From f56a646b8e4749750d432da6d4d979e199a6963f Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 8 Apr 2021 13:36:15 -0400 Subject: [PATCH 124/131] [Metrics UI] change composite.size of snapshot ES query to improve speed (#95994) * change composite.size of snapshot query to improve speed * use value from kibana config to set compositeSize, clean up unused config properties * fix test * change config name * fix reference --- x-pack/plugins/infra/server/kibana.index.ts | 45 ------- .../evaluate_condition.ts | 38 ++++-- .../inventory_metric_threshold_executor.ts | 15 ++- ...review_inventory_metric_threshold_alert.ts | 15 ++- .../metric_threshold_executor.test.ts | 5 +- .../plugins/infra/server/lib/infra_types.ts | 12 -- .../infra/server/lib/sources/sources.test.ts | 5 +- x-pack/plugins/infra/server/plugin.ts | 5 +- .../infra/server/routes/alerting/preview.ts | 4 + .../infra/server/routes/snapshot/index.ts | 10 +- .../server/routes/snapshot/lib/get_nodes.ts | 57 ++++++--- ...orm_request_to_metrics_api_request.test.ts | 115 ++++++++++++++++++ ...ransform_request_to_metrics_api_request.ts | 23 ++-- 13 files changed, 236 insertions(+), 113 deletions(-) delete mode 100644 x-pack/plugins/infra/server/kibana.index.ts create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts deleted file mode 100644 index 35d2f845ac4c1..0000000000000 --- a/x-pack/plugins/infra/server/kibana.index.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Server } from '@hapi/hapi'; -import JoiNamespace from 'joi'; - -export interface KbnServer extends Server { - usage: any; -} - -// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP -export const getConfigSchema = (Joi: typeof JoiNamespace) => { - const InfraDefaultSourceConfigSchema = Joi.object({ - metricAlias: Joi.string(), - logAlias: Joi.string(), - fields: Joi.object({ - container: Joi.string(), - host: Joi.string(), - message: Joi.array().items(Joi.string()).single(), - pod: Joi.string(), - tiebreaker: Joi.string(), - timestamp: Joi.string(), - }), - }); - - // NP_TODO: make sure this is all represented in the NP config schema - const InfraRootConfigSchema = Joi.object({ - enabled: Joi.boolean().default(true), - query: Joi.object({ - partitionSize: Joi.number(), - partitionFactor: Joi.number(), - }).default(), - sources: Joi.object() - .keys({ - default: InfraDefaultSourceConfigSchema, - }) - .default(), - }).default(); - - return InfraRootConfigSchema; -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 5244b8a81e75f..8cee4ea588722 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -33,15 +33,25 @@ type ConditionResult = InventoryMetricConditions & { isError: boolean; }; -export const evaluateCondition = async ( - condition: InventoryMetricConditions, - nodeType: InventoryItemType, - source: InfraSource, - logQueryFields: LogQueryFields, - esClient: ElasticsearchClient, - filterQuery?: string, - lookbackSize?: number -): Promise> => { +export const evaluateCondition = async ({ + condition, + nodeType, + source, + logQueryFields, + esClient, + compositeSize, + filterQuery, + lookbackSize, +}: { + condition: InventoryMetricConditions; + nodeType: InventoryItemType; + source: InfraSource; + logQueryFields: LogQueryFields; + esClient: ElasticsearchClient; + compositeSize: number; + filterQuery?: string; + lookbackSize?: number; +}): Promise> => { const { comparator, warningComparator, metric, customMetric } = condition; let { threshold, warningThreshold } = condition; @@ -61,6 +71,7 @@ export const evaluateCondition = async ( timerange, source, logQueryFields, + compositeSize, filterQuery, customMetric ); @@ -105,6 +116,7 @@ const getData = async ( timerange: InfraTimerangeInput, source: InfraSource, logQueryFields: LogQueryFields, + compositeSize: number, filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { @@ -128,7 +140,13 @@ const getData = async ( includeTimeseries: Boolean(timerange.lookbackSize), }; try { - const { nodes } = await getNodes(client, snapshotRequest, source, logQueryFields); + const { nodes } = await getNodes( + client, + snapshotRequest, + source, + logQueryFields, + compositeSize + ); if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index d775a503d1d32..8fb8ee54d22ab 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -73,16 +73,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = services.savedObjectsClient ); + const compositeSize = libs.configuration.inventory.compositeSize; + const results = await Promise.all( - criteria.map((c) => - evaluateCondition( - c, + criteria.map((condition) => + evaluateCondition({ + condition, nodeType, source, logQueryFields, - services.scopedClusterClient.asCurrentUser, - filterQuery - ) + esClient: services.scopedClusterClient.asCurrentUser, + compositeSize, + filterQuery, + }) ) ); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index f254f1e68ae46..00d01b15750d1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -32,6 +32,7 @@ interface PreviewInventoryMetricThresholdAlertParams { params: InventoryMetricThresholdParams; source: InfraSource; logQueryFields: LogQueryFields; + compositeSize: number; lookback: Unit; alertInterval: string; alertThrottle: string; @@ -46,6 +47,7 @@ export const previewInventoryMetricThresholdAlert: ( params, source, logQueryFields, + compositeSize, lookback, alertInterval, alertThrottle, @@ -70,8 +72,17 @@ export const previewInventoryMetricThresholdAlert: ( try { const results = await Promise.all( - criteria.map((c) => - evaluateCondition(c, nodeType, source, logQueryFields, esClient, filterQuery, lookbackSize) + criteria.map((condition) => + evaluateCondition({ + condition, + nodeType, + source, + logQueryFields, + esClient, + compositeSize, + filterQuery, + lookbackSize, + }) ) ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 9086d6436c2a2..44b2695ba4e3b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -424,9 +424,8 @@ describe('The metric threshold alert type', () => { const createMockStaticConfiguration = (sources: any) => ({ enabled: true, - query: { - partitionSize: 1, - partitionFactor: 1, + inventory: { + compositeSize: 2000, }, sources, }); diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 08e42279e4939..f338d7957a343 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { InfraSourceConfiguration } from '../../common/source_configuration/source_configuration'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -28,14 +27,3 @@ export interface InfraBackendLibs extends InfraDomainLibs { sourceStatus: InfraSourceStatus; getLogQueryFields: GetLogQueryFields; } - -export interface InfraConfiguration { - enabled: boolean; - query: { - partitionSize: number; - partitionFactor: number; - }; - sources: { - default: InfraSourceConfiguration; - }; -} diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index aaeb44bb03aa7..0786722e5a479 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -134,9 +134,8 @@ describe('the InfraSources lib', () => { const createMockStaticConfiguration = (sources: any) => ({ enabled: true, - query: { - partitionSize: 1, - partitionFactor: 1, + inventory: { + compositeSize: 2000, }, sources, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 50fec38b9f2df..f818776fdf82c 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -35,9 +35,8 @@ import { createGetLogQueryFields } from './services/log_queries/get_log_query_fi export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - query: schema.object({ - partitionSize: schema.number({ defaultValue: 75 }), - partitionFactor: schema.number({ defaultValue: 1.2 }), + inventory: schema.object({ + compositeSize: schema.number({ defaultValue: 2000 }), }), sources: schema.maybe( schema.object({ diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 4d980834d3a70..3008504f3b06c 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -29,6 +29,7 @@ export const initAlertPreviewRoute = ({ framework, sources, getLogQueryFields, + configuration, }: InfraBackendLibs) => { framework.registerRoute( { @@ -56,6 +57,8 @@ export const initAlertPreviewRoute = ({ sourceId || 'default' ); + const compositeSize = configuration.inventory.compositeSize; + try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { @@ -96,6 +99,7 @@ export const initAlertPreviewRoute = ({ lookback, source, logQueryFields, + compositeSize, alertInterval, alertThrottle, alertNotifyWhen, diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index cbadd26ccd4bf..c5394a02c1d04 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -40,7 +40,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { requestContext.core.savedObjects.client, snapshotRequest.sourceId ); - + const compositeSize = libs.configuration.inventory.compositeSize; const logQueryFields = await libs.getLogQueryFields( snapshotRequest.sourceId, requestContext.core.savedObjects.client @@ -49,7 +49,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source, logQueryFields); + const snapshotResponse = await getNodes( + client, + snapshotRequest, + source, + logQueryFields, + compositeSize + ); return response.ok({ body: SnapshotNodeResponseRT.encode(snapshotResponse), diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index ff3cf048b99de..21420095a3ae5 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -19,18 +19,26 @@ export interface SourceOverrides { timestamp: string; } -const transformAndQueryData = async ( - client: ESSearchClient, - snapshotRequest: SnapshotRequest, - source: InfraSource, - sourceOverrides?: SourceOverrides -) => { - const metricsApiRequest = await transformRequestToMetricsAPIRequest( +const transformAndQueryData = async ({ + client, + snapshotRequest, + source, + compositeSize, + sourceOverrides, +}: { + client: ESSearchClient; + snapshotRequest: SnapshotRequest; + source: InfraSource; + compositeSize: number; + sourceOverrides?: SourceOverrides; +}) => { + const metricsApiRequest = await transformRequestToMetricsAPIRequest({ client, source, snapshotRequest, - sourceOverrides - ); + compositeSize, + sourceOverrides, + }); const metricsApiResponse = await queryAllData(client, metricsApiRequest); const snapshotResponse = transformMetricsApiResponseToSnapshotResponse( metricsApiRequest, @@ -45,30 +53,39 @@ export const getNodes = async ( client: ESSearchClient, snapshotRequest: SnapshotRequest, source: InfraSource, - logQueryFields: LogQueryFields + logQueryFields: LogQueryFields, + compositeSize: number ) => { let nodes; if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) { // *Only* the log rate metric has been requested if (snapshotRequest.metrics.length === 1) { - nodes = await transformAndQueryData(client, snapshotRequest, source, logQueryFields); + nodes = await transformAndQueryData({ + client, + snapshotRequest, + source, + compositeSize, + sourceOverrides: logQueryFields, + }); } else { // A scenario whereby a single host might be shipping metrics and logs. const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter( (metric) => metric.type !== 'logRate' ); - const nodesWithoutLogsMetrics = await transformAndQueryData( + const nodesWithoutLogsMetrics = await transformAndQueryData({ client, - { ...snapshotRequest, metrics: metricsWithoutLogsMetrics }, - source - ); - const logRateNodes = await transformAndQueryData( + snapshotRequest: { ...snapshotRequest, metrics: metricsWithoutLogsMetrics }, + source, + compositeSize, + }); + const logRateNodes = await transformAndQueryData({ client, - { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, + snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, source, - logQueryFields - ); + compositeSize, + sourceOverrides: logQueryFields, + }); // Merge nodes where possible - e.g. a single host is shipping metrics and logs const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => { const logRateNode = logRateNodes.nodes.find( @@ -91,7 +108,7 @@ export const getNodes = async ( }; } } else { - nodes = await transformAndQueryData(client, snapshotRequest, source); + nodes = await transformAndQueryData({ client, snapshotRequest, source, compositeSize }); } return nodes; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts new file mode 100644 index 0000000000000..1e1c202b7e602 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { InfraSource } from '../../../lib/sources'; +import { SnapshotRequest } from '../../../../common/http_api'; + +jest.mock('./create_timerange_with_interval', () => { + return { + createTimeRangeWithInterval: () => ({ + interval: '60s', + from: 1605705900000, + to: 1605706200000, + }), + }; +}); + +describe('transformRequestToMetricsAPIRequest', () => { + test('returns a MetricsApiRequest given parameters', async () => { + const compositeSize = 3000; + const result = await transformRequestToMetricsAPIRequest({ + client: {} as ESSearchClient, + source, + snapshotRequest, + compositeSize, + }); + expect(result).toEqual(metricsApiRequest); + }); +}); + +const source: InfraSource = { + id: 'default', + version: 'WzkzNjk5LDVd', + updatedAt: 1617384456384, + origin: 'stored', + configuration: { + name: 'Default', + description: '', + metricAlias: 'metrics-*,metricbeat-*', + logAlias: 'logs-*,filebeat-*,kibana_sample_data_logs*', + fields: { + container: 'container.id', + host: 'host.name', + message: ['message', '@message'], + pod: 'kubernetes.pod.uid', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + inventoryDefaultView: '0', + metricsExplorerDefaultView: '0', + logColumns: [ + { timestampColumn: { id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f' } }, + { fieldColumn: { id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', field: 'event.dataset' } }, + { messageColumn: { id: 'b645d6da-824b-4723-9a2a-e8cece1645c0' } }, + { fieldColumn: { id: '906175e0-a293-42b2-929f-87a203e6fbec', field: 'agent.name' } }, + ], + anomalyThreshold: 50, + }, +}; + +const snapshotRequest: SnapshotRequest = { + metrics: [{ type: 'cpu' }], + groupBy: [], + nodeType: 'pod', + timerange: { interval: '1m', to: 1605706200000, from: 1605705000000, lookbackSize: 5 }, + filterQuery: '', + sourceId: 'default', + accountId: '', + region: '', + includeTimeseries: true, +}; + +const metricsApiRequest = { + indexPattern: 'metrics-*,metricbeat-*', + timerange: { field: '@timestamp', from: 1605705900000, to: 1605706200000, interval: '60s' }, + metrics: [ + { + id: 'cpu', + aggregations: { + cpu_with_limit: { avg: { field: 'kubernetes.pod.cpu.usage.limit.pct' } }, + cpu_without_limit: { avg: { field: 'kubernetes.pod.cpu.usage.node.pct' } }, + cpu: { + bucket_script: { + buckets_path: { with_limit: 'cpu_with_limit', without_limit: 'cpu_without_limit' }, + script: { + source: 'params.with_limit > 0.0 ? params.with_limit : params.without_limit', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }, + }, + { + id: '__metadata__', + aggregations: { + __metadata__: { + top_metrics: { + metrics: [{ field: 'kubernetes.pod.name' }, { field: 'kubernetes.pod.ip' }], + size: 1, + sort: { '@timestamp': 'desc' }, + }, + }, + }, + }, + ], + limit: 3000, + alignDataToEnd: true, + groupBy: ['kubernetes.pod.uid'], +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index a71e1fb1f1f14..811b0da952456 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -15,12 +15,19 @@ import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapsho import { META_KEY } from './constants'; import { SourceOverrides } from './get_nodes'; -export const transformRequestToMetricsAPIRequest = async ( - client: ESSearchClient, - source: InfraSource, - snapshotRequest: SnapshotRequest, - sourceOverrides?: SourceOverrides -): Promise => { +export const transformRequestToMetricsAPIRequest = async ({ + client, + source, + snapshotRequest, + compositeSize, + sourceOverrides, +}: { + client: ESSearchClient; + source: InfraSource; + snapshotRequest: SnapshotRequest; + compositeSize: number; + sourceOverrides?: SourceOverrides; +}): Promise => { const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, { ...snapshotRequest, filterQuery: parseFilterQuery(snapshotRequest.filterQuery), @@ -36,7 +43,9 @@ export const transformRequestToMetricsAPIRequest = async ( interval: timeRangeWithIntervalApplied.interval, }, metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest), - limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 5, + limit: snapshotRequest.overrideCompositeSize + ? snapshotRequest.overrideCompositeSize + : compositeSize, alignDataToEnd: true, }; From 5b1ce4d2e380bf3387205e7f65c2132fc05ea32d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 8 Apr 2021 13:36:29 -0400 Subject: [PATCH 125/131] Docs: getting started with Kibana Security (#94158) Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gail Chappell --- .../quick-start-guide.asciidoc | 4 +- docs/redirects.asciidoc | 6 + .../security/images/role-index-privilege.png | Bin 80869 -> 0 bytes docs/user/security/images/role-management.png | Bin 161191 -> 0 bytes docs/user/security/images/role-new-user.png | Bin 69595 -> 0 bytes .../images/role-space-visualization.png | Bin 127770 -> 0 bytes .../tutorial-secure-access-example-1-role.png | Bin 0 -> 320953 bytes ...tutorial-secure-access-example-1-space.png | Bin 0 -> 328089 bytes .../tutorial-secure-access-example-1-test.png | Bin 0 -> 183305 bytes .../tutorial-secure-access-example-1-user.png | Bin 0 -> 219096 bytes docs/user/security/index.asciidoc | 1 - docs/user/security/rbac_tutorial.asciidoc | 105 -------------- .../how-to-secure-access-to-kibana.asciidoc | 136 ++++++++++++++++++ docs/user/setup.asciidoc | 2 + 14 files changed, 147 insertions(+), 107 deletions(-) delete mode 100644 docs/user/security/images/role-index-privilege.png delete mode 100644 docs/user/security/images/role-management.png delete mode 100644 docs/user/security/images/role-new-user.png delete mode 100644 docs/user/security/images/role-space-visualization.png create mode 100644 docs/user/security/images/tutorial-secure-access-example-1-role.png create mode 100644 docs/user/security/images/tutorial-secure-access-example-1-space.png create mode 100644 docs/user/security/images/tutorial-secure-access-example-1-test.png create mode 100644 docs/user/security/images/tutorial-secure-access-example-1-user.png delete mode 100644 docs/user/security/rbac_tutorial.asciidoc create mode 100644 docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 1bdc9b9dea859..5e6a60f019bea 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -12,7 +12,7 @@ When you've finished, you'll know how to: [float] === Required privileges When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. -For more information, refer to {ref}/security-privileges.html[Security privileges]. +Learn how to <>, or refer to {ref}/security-privileges.html[Security privileges] for more information. [float] [[set-up-on-cloud]] @@ -141,3 +141,5 @@ For more information, refer to <>. If you are you ready to add your own data, refer to <>. If you want to ingest your data, refer to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. + +If you want to secure access to your data, refer to our guide on <> diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index e4d2b53a2d8d6..5d0242ae31950 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -286,3 +286,9 @@ This content has moved. See {ref}/ingest.html[Ingest pipelines]. == Timelion This content has moved. refer to <>. + + +[role="exclude",id="space-rbac-tutorial"] +== Tutorial: Use role-based access control to customize Kibana spaces + +This content has moved. refer to <>. diff --git a/docs/user/security/images/role-index-privilege.png b/docs/user/security/images/role-index-privilege.png deleted file mode 100644 index 1dc1ae640e3ba362816d3240e4f7ed074415ae7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80869 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≫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 diff --git a/docs/user/security/images/role-management.png b/docs/user/security/images/role-management.png deleted file mode 100644 index 29efdd85c4df39b6fd1b756878f0784df4c3b036..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161191 zcmZ@g1yCH#(zpeOpaFuryNBTJ?sCE1?FbS`aCZ&v?jC|3?(TZHJN$g_{qm}ke`~gC zZ)SR?WwxiMyZ3%5DM+Fq5gL;jN;A+57#kZI4UEv!B00IMeE%M*?|9 z>#KfWSC>zi|3()#L>PooU6x4(%o}qN9i+6KAt2si{c}S?q^9G&@g3GuP0K|~UXIVy z-j>PO%-#gRq_Ox>`_F%MgCja*!|1*vlz}eKv(!s^j-tOZ+;~JaTySfOH zk^M8#|6c#b6X0R_e>2%R|2J801Tz2A!pzFV!u&Td7t1gI57<8~{|5UPUH_g=;Ge`H#B)?k~Xn52OBrasQUeziQvaB7`Ks{J$a=Lc+crC4qqW1R*UZqUHg4 zlmYL9(>vRTMjM1__3>&orr_(>P$XQn&`?;I>M$?cwfe*Qxx;$*b~kZx>?%kyq_;~7 z>tNFRlu~fbb9Y2K_*xzT(*!f~UfRulcQ_^Gb-3a*U65Uuft8k1A1X(SHR2OPBu>B_ z&Z}mWMv0+>(p*kFemshSsC!eBQOb*}(rRZP?`MT;H0dDbxj9(g*F|A&B3`0$rs%<9 zA*!UfaKlJLH2y|Y1*e4>ay#U^KvX}<&8_jgIeOiZm$3%#Q7P|4q>aG6E{1G`a! zOBg0Eo=rAVk^}OcL~^stAYrrw^anx(Nw*qc$8!2!UjO7#GNG?0qcaUa6o+)48Gb58 z)yvH?L`TET>oX_n^GL+-#=xWJ^Q9=`+!*1>via8G%~&mH?>1v_i8Z-D>UK;X)e+~& zbHi|{naG#uZL%w)^ueWx$v{cj{)vFKFJbJ>M-|SYXuIFa63hW9;Y7Z^Jlwot{jBL_ zgRoOiKHY5_X3m7>UV#_g)^YEM1D)J#CF9TxbbS>1(=WE;;WE0f*6S0}^xG08cp3EqBB*Ar>wo6a(8NT^LNw|2S5DS%84f|b9TEv->Kn8OPWhU zNy=1B->D}zbF_o>#cXTf;D@{1osQ}jNSup&8~i$F8rCz=EAZinud$|JEHZHo>`G%h zRxCGlND>J6k|dm-et=j2Tj@-1et(p#FBEng1tfl5PC6fRjzuikARy&_Mvr~E#6tvg z@V;8Gxm`^K>;W?Z-pJX$-U)08m`fD)Kpy(m+91#@&#!jx;1%d7`PqD)EmYcz4~d^e z3}9fTlNzfiqYzW4mj0~|Ju;Dzr@#k4Z-LVVZVUr=IEp9e=qf~|cG8iEq2KHmk-U8q z$8Xp}(+ix_M#u{{Tc-|G;gJh+qHe{W`uYu{J*mbTIdiwUf_IWIopPORN>&;~KJ8TI zjv|6U1OD5nZni+nCREK}X7jyS1(50aMkJN>_06I21yHAnN4AlJKb1)j9)viQTt>tw zxIFPjW?+p}EqYmQ*4U`L5REk-!%D?7`320BeAGdRkK0}c6aVcstZNE!T=kWaK^V8E zY@;d5>XmlOPNVe)Uwqj{p4OWkbn~4%$$>jdAhB%E1lmFZ)^wl%P+Nl;WX~6P4&N#v zME|xh`%+#UT&wEVC(JNTvmTu^{7TJPM!?1g9hpW3t-+`u^F1D&RR_sD^1GcNlh1zg zS^4C%bAi@z-OVJwcFw09?OL9kf0M#o>gqMQB$1x=yF6aF+5OZA?uB16IiRw4YT{aW zgMO@Ok~jm1vT8eZ*F>Wd0ox9c!H7dCcKi?bPdrnmjtfM-yrofct%!>IyRPB`UMIwo$@W+)4on|2MK(H*g_SDYa z`3~dirrhdZ|oTpuSJD!yYhw=QgTf&gH?9=XUFOIkJOc6aDEz&Ygzx(4NJwsg25(uO_bA zC61d{PgnmJQOpTwlb|~ZrPpi_7WA3VCOoFFy6JcoxJLZ@aZZETaCNkN_~=5brLrfg)_E#PwK@8F zDiE+(e{kXYcpuB_~4Y=zkO_ReFhRqJ-B#7@M4iwt1&x0!T7?BgBN6fPGx`a z9c}2&F}hYD1_MJyfKP?&3c)+nnQ!8Xrd3KoK7n!RcHreVg|uv~jZ*(7D2l%oNnC$~ z`t#U#y7-7Z z@}^7isbw{66Q2!;WI-5TrVsE{eJ33Qff)U_@??pot-qKH>9>rC`2Y36gRD(6V?3_L_R7OUB$oaw zTe~n2Qe|rrZ_v^IK2P+g1{fo?98yYM0lB}?hRL&tc*mc!o*o>Z>nm3E{8!mGQ+*PZ zsZdsq{<~nwy98At8;H%l|BB*8IE0|e1v2IPzmka!fP%Np!ysY0TLEPAs-M)EBC1@VNf~8Z-X-zB3&~ULt9RZuhExgZUxbZG z^8jNZMf>18j1&7;!HGk?5YsA&hz=RT4BGsaUE*0^J0+2HWJP5rPMsV6%CwJB2xj;y z0UdOnY7jGjT_Dk)jLD6@VDkTb;TdDggYi{I(tW3G_~+vMr)GfsUvy7z3V@up)#ZHu{~$C$)|D?PUMjpm@HaI4eJI|V?-A0(|0+dn zCMdl{)uxBAddk0X{f-dP!E$U3Axk{&53YaEhnW;~hvRSD>Xu4QCvuJrkyCI2re?rL zZCs^==fhSq68Ae;ZfrgYbYCtH>`71+aaUGMWGH*)Gw=&qT3TXaV35AUWsH?12FjJF zm(n33{y@?S)2vnS(rKA4QCD4Tu%$28sgI{q-^|xyU0aRI*bVHD`oOEx7eUAj&D^3C zX}>m_kfPawX23`JC+i~JpkA(KM~^*NxTwtg)3?#6whEU#&+rsF2eow07B?M{lW>?u z$FKy(B7E%30=&@n7Hhxpa!M{onq=I7@-ZSKCtz`@1AzQY{|V-}(&;&;s^?d1!Q{6if6Jyg-r@pRs| zlrjXnS-ciK6w8xu8yBycv-H1EBn>nIe5u;)Br$Vv>RNS zeJ)4vJi*1(A6~pq+a-q*86T-lk-{lP(|U7Ja;13`D=k%eoxxUDpgGU8!}mg+*&=wNvpOxua9nTfY<91uN0 zK{syIUb>mT^C%Wt0AWW9t(p-6#VP_VO={&j>M}`8I~60BeHZ6ae<(+#JH#f>(ow_0 zQ3FcX(u6qOF-yke@`8PkUxbj+%;ybzTT%;uHbqsamTQ)w>=^qf%jVWjY8|#_=wpx+h+`ivl2WVSa_!k+Nqi{csN`Loju%~Hfw<|^dzsFW92QE#Q#4zc(0)pZ_+m!k_ZOv=+lLdTW>@=_ z>1O?r8Ts~W&O3EeEwW)a{bpV%*>)_^Wd`j!5|IRqalljt62GS$&{Tu1q<+P;E*$Cy znrraP$n+`ZeaN2+%>sSEL3X~~xzeTBfNttLWJ=dRmZrCr0f`L^Scl8Q>= zEFnwRzFpgD+KMI%m56~leO#Jf{8CJumwZ(4c6F}}K)}T-K@8khZX`X?G>nLSJZaU$ zVKyKe6MPN-tzMRF`Sl#M%ln7^|Fl7kzKBL?rtX+Y^Q}|*TMI1Ltz^`P;&v;aHJtQU zl;HTX5t9*%0P>gaL({#{j0&v7>0H4y)VUuDZbrGO8FTD zhvn19^$*?1s7npDaY+WinO;uYNwH0OO}9NmpYw#ar^$Dd^@o;D$MthEy`k^*1YXoC zKIFnhzcZT|Qok4=N0WZM=`SVSmRVt^q;gI{o0Zf~7PvRRA2W^Gr%oh^mt}50g3!N{ zK~Kc#c5vRyXaxQKft5}Qc)TI1v0LFQ{As_|fk{GAf=gDFzTb8^mUKRv-{$?ke5CCra6*l>{cF7Y8)oWXw_?iPN-vdkuUs7*)F-I2>G_~`)z#NOW)|>Di)`>5unv?sZ000(Y{!5 z-`eg=Bc85&PCrB@(({RAHgIcvo{SyCcB_TxT z(a3M_Z6D!^BjU7*XPUr2NIdUSrO?M^-FU}yIAB-3W+=Dd%MaQLCEQ3t6OiC zP^MKqnK4(Y*-vDg$0GU-U&Ly*5$T8)s_!5mm$R!0zWX#hj*HPFuURigL&~8di^(`Tb`ryvX~SW zTBUIq_14=AJ|9GP+oJP*HVi#^Xtg1}ponv$C@)a=+*##nZaMh^s={d zP6*9{gBqDf--TKJe46;L4+OmnckOgsN=dv!o=x zhn4O}e&KP%eSlwOL7>j``1xp7J=i?(;U&+~mv!7|{$Qmv>9Ns%P3Zo(k@w;mO?V(r zI&M2r*Ul|%-@xzLbu_oH(iF8MMeGmNj6MQ%g&t}$OSJzars${bycZ%;>H+;sO zqOiPE+U_P{In0F-R@f18f>G~kv)e;L$_Canj7t6V)LP6?srmU16YQ~2f<30mIE;w->+C4C0dupzl?ry{=u z_z7!|h0Ta5oa6WX`2Zf5K0x^xn{)D`&jSd_(q-%x6aW2zOZV3RzmnN0LOOo_+~66~ zQs5n4cw!D*Tp79ZYN?^H$ixd!F~=ZpW)C#5j)$_W#IqKWVlS!)JPR-$I$Jlg2t&ZPzX6M}h&~ z83#N`Cc%;Fah&}ct~|)bLO;^_EU41r`WOOS*%NDic}UBbOJSR=HAict=tl%+aazv} zIgbf3j|n~22|S&V+aZn*r_`EteSJ|dn=MWE6tLK z^FaP#2WH(BGO#-ypt}=$`(-FDPzQ9p*D#nj*{W~55<;dj>StWSP^2pj){IOr+^vXt zsNXWXOkAn)oKh%OZ4BioS5|PNEEsy)NuxC0Gx))PPcBdRTNa>CXDSgdNPEEMcR;n! zYMYc-T%GSVA~RX!#hcM7F%UbW3z(=GX?eA%^Ld^kd;TPwURSI+IAiLLd1V*#%zwkv z9&y+&n=sGNo{}&Vhq81tG!)V5vNmy1W7R9MQ0TctqAI#M6d!%viV2u`IIb~YvZK|{ z37;-i7AcAUF*tOhO;Ocmemn}JUGLU`Et{uQD;J3OkG>gOCFJTrX07 zF%oUkhrHx63vXhXu(>(yasKs0WKy*+;?;l;JedN1NHM`xQ! zpfmbrmmYJqd`qwi0UpDrtq2uL#QVAMulR1_wY0eKsuO#hk$UcT!*3s$XC~B+@u_aQ z4l37pVgHDo5^oYQrC+I5-}bP|QSj zs9rG25q+H!%0AkH(`k9OFo8o=RC*^?xyENni1&KN(#hah3%^>Tv5|(DbgP1vx}H8E zaS8sF&i<*yYv|x5E6h!%QsMk!w4PeV=U%18)G)S<$D}~uYz8R?{6L!MOKXQRL~-rK zTP==)&u^dG#%5Zp#>nIZx~1PPi98sMuHj}CKOtfhHFyGKfMd(8UtRFrfMH$gif?i|4h07h)*p>cf(j z#)LJ%eF?E@92&2I#)!rTiS16O@SQ{Bl&#Z`B3n48v3`b=YIXvp32O9&s@dVR-7i9$ z!Pw65H2{`Yw__JO;aNmC4to85)c(?>0qu*$Qf5%KtokzC1k9g#kVn2P}KaSW@ehf1WXvKcc70 zlbED6R4g%3&(LQzPn6g;h2}H72PMRYwQ@QW%#|-u`dTF>Vx--Gm|wv(t|duHp*Ewkw%8Kx^#VS#_b{=Vi=Px?l`;74E{RqNprOOp!_oPqY|B^g9csO}qMR5E~zsop~JM`}%4($mg)RpB8_#u-pTbcpbS`f7T! zbioQRc3ML*e#g2&y3m|jR} zr?6~~4CPKJyUzX%p-xev{&>JFc)&X^*<7Ob%gCFC(+(IzSekPl#)35(wuR6706Z+8 zb7h~G%s1%`P08X4maEJZ+)0uyH#=VBtZ|y5JN|A0PEqOc^q($P)m%v+%b4gLYTE^i zIEi$2s1&PwsNo;OcJ}-=nWLQB5q*6)J2*5`n(>wckf<*&Xk<05)JXAsfo7Kb^Jluk z^kajreas8))q9WbvRP&rk&Ya@Al(+1vKH|fmxqz>TS(g^e_CQcMIhMsMUe+g;J&Z$ z-^xyf0+wa{4CRAPD4|ScRAfrxX^8VLW%Pq<*_Gk4=05Xrm4n4@sVG1h!sW4&d~hlP z%4i;auG~2LV&#sY-$m5JLe~<_JB0~O_IoWVm3D0uC94uBB?DcmV9-e1>;OtTR-RRJ ztPcDfre>1aN|6RQDWgG!;@$S_ z%Yn*g5SG3dHc@`s>WO^f4=e14PGG;Z&yEvj@dF`88I^uiB^5}+=Dp7t4V6)sqs(i8 zaCe39CNV8bV%JCiXQ{=yK^^!+{m1Gnc#!GY7}_`_v~&hv{YvK+e-MKXOQseJertt6 zd&@cr)D2x97rzDxvI3s**=bXQt-e=$qAg_0@OYq;@kd(ShHwu1)#c}&IiQjz`(hIt z8`y&HFRGHq3$ZH;D)K$T&ZY_As-@k|t~hQ`Deq;*GH;YVjF&t#Z2EhKl|zv(23)w3 zl)@~4@6c`NEGrK1dyL*Jq(GA%gZABYuBclv29@+)VXw(HsF>btbDNUQS~g%SBk`h6s6*or!@H6)zwc!k4xOGODKwSDDjN0 zew3k;Q_dKf8V?5RU-UFkc;-q2aKDwaSK*%&%Wv*naDN$EEcedya0w+pQMT-&}M+cRT9PwCXzaVZ{$W9(G;7MpVELBDs(2ls4JqXUGoHtT!Gu8{0c)=EZ- z8-MNE<*`91Xte#wOUa%`kY?)ObJ9LU7jHNnPBc7e(K|r!)i=0i&zZdZ!}V!~XtZ6{ zjKA0p*hEk{+FLpGgRUW>8Ws2UzD01`tRq(}^H64{k8GL+R z%O7pLt`TCz$N^s*A%h8F+^3lrZY=cW?T!}o+)l?P33D#DOWwOyzDMb9W)iYJDkEW1 z&tnkKpN6PaZ#N>aN-wP>Zak4jrzK*{m%&cT7|0#zY9I#4_JqbUX=cpG@o~>Rt1mAL z#8>7G^(8TGa4ZcagqvJEe+9x78~K@QqenUUk{7(r1(F~7N$@%mu8^u2dI!E=gWb&s^ zyxz?&wzSj)?gar$|8fJfHZcLKq_sfT;#rLRe^I>;nq@kU>H*k)cN9 zMCBt#Db}8VB#zDEp^N;zY3*-y22w@+d5uM`(4%2z4ryWgUNaa?E5Vrj;y-VF`ZHC*?N z7=2p9ru7`dYcAE7zm}WWVeG6(oFTj!nwxWv_juku3)V-hQSWSuD>0zOktt!_eg-n; zUHVZC)b;m+5~ybrW^``{1AF!cLD0G~UZvp4X3O`Uwwp+v4)ZpaYuT~kX{`zd4;GnM z`#Sv2mWt(8JK}EC1JLBaxZ0(;1g4SAotaBrslt9>G=3D2dTyVX;n1C8z_(|2D6FMZ z8q7NLSps^VgRNj~ln|g;V~qt4Ih<^Hm9DIiH|u&WH~_~KPqvGzH?p3z#%X{g=juYp zLm%bXIw<1vj_GnRo?E#S{7^F~OY7VGiL5}w`MTQ&#_zUsb@_)7QRrJKm0=w!wawK+ z+uod+Byut`6Y^hN$O%?xas4HrLj3>Yp^)mr=(@mS_?FUc5qQFk$tZuQ?vzQ?tPKp{ zVp4LHJub{iM0sq}xlo(mSlKMBBiiwXHq9gqR+|J5GhtI0JYt5rN0)G8p+y zC&6Urw}fMj(~d^U>m$L!PXqEm1$%}hSKBLi9lswwk|So^}t#5eEFPFuw_ zrAA}!{&Gt}{Er`#BgUrrPI9a%9HtRR(((yYItLv&57(J-gqn#nr8(~9Hj~fNOv2?c zdaL=>zx#@~!)BKnF^-f(hdXRrcS!nY+zhb-w!%ZDKsxX9EE6W#+|S`s*sLS4Wn~lK zAL?UXGQiDLHZ$_}Nk@y!&x>=hPWhp-tq+bWnw{gdUv_X(9+q?#(vlfoPu6tcTb=^-A@UD@5)sP37JYuO% zx}UBxr@5cR>|U3?AWr7sh>ciQZsTRRq2_w|y|(q58q7{h2P2`IrX>oG%D(%`x?tQ1VAb1drgb&Jr{O%(LvTwvbEN zT`pf4#aRxxbD_eK=iOlrL@JTCd*z5Fu+vMD?T%7+nm0O%axGbY&VQ+=+Z3?mScLtZ z%kVqxCGcQm0F27Ql&#k}Dpl1W!WvGabrI+^oPA#Cllq0hNX+s2di$B}-DI;ywZmzQ zO0-c8)1S@{Nq9&+(iba-VA$b4_RkI^=oH;Nm`qv*<}m(xpN2d7wvag&4#qt|_J9v^ zm4h1yfznlavSZV^urN#LOojz*bX6^B6#-r_t)094s!F=Qz5Wt6?TY#M72gFC5%O^` z%L=f{=qEGqeSpc&?R?>2kmE?E6H7wj58W6LoB z#D;kH9C7KxofpKFI#{Ndnf_@nW}L80514?(aBI;Dx5u$khxi)V(8cPu?Y$hqe8)#D zfl$69AV3tN?2`l1neTHoF161gL@#6cZ~Gq`66%Rk3x@9;Jr(`v)Mw3RmaA4o+^2Mu zv~EN3r{r4*4@txqZAO?jJ@(TFU=;~syT1w2Nd7sGPRfV!>}kKdNs>W_lj7l=r@LcA({&@ItN7kh*j)HXj6-+GPYz8fWdz!E9C0r=lT!Zk5E1 z({|n+bY||P4HPCwM9aqsC$kAJUu%+1WHfxg__@_ct$bh_D%|+pN_KdE3dmY2ozT8j#s~wqndSF;`(U)7tLV-S#;aKo96C7?Dg5OnB6MhZGTGQsc8@_3Ss) z*I5#L|G(bZP&)q}UqVf_<`01509SHD6xy&PhJ?dY4>90_XGvaOK?`?J2;UFH8;mO- ze!HJ?S*%NxL?>tm*+9J|%s6G!zCq@xFTq&6c|6waS3s@{!VI2DfxM??jL|Cn9f`V6 z17Bg?EDaisbA5QO*xw?9H1*bV4Ct<)D4F@D41RlmpZn{^*yQx1(E93F5?2~901qUe z)-b?c*+H$_X!j^9zgq8I8BTPY`*ic@a}y&ue)mgBXcJ_g-aRDX-WNV&zBi4Z>9U&j z^QoI8D*BpG6ppZTEGOPb_J8Od|J;Bs48z4hL|-wn{j~8n zg5#2l5T5<{P9?mtGy#)4-dyCD#G_XEq) zd`e!nv9}SA3uCge&XSWib2t(t@1+N&G2i1;#>8m5kq;D>U{H~4fL&1MvkKfeYUX0q z2*}cNlO3)!T`=F z1gx7~f9pu^69j*Ke0LlAdUSSJt=)dp@qyg{JX^t}zDdu&QbgDn$6taPj$1c}mgAT^ zmd|3kkkr$>vi0KI=6-5$58ND^EOQk7QK>d@&1=T0vFuy^A;4C=Wo#tV98c6J;C_8;37Xmr3g<<)e2y;ZAE@2hRqv%P@RKKi`tf~ulbT9X#tUU7`12e zkn7#Ywyz-HyDxKz^}=EzD}Kw=*>#GIVOKkAMYNQGO-q(G_fY{61O|cm^#!2 zG?%rpDNAz@fnQxjB@$B6AD9~&ACmyprF4R>HT5^CjMsp|${mlu*thfq#Yk)4@T&vg zmQw!314R#|Vp=dM`%|gC%ishT?{(fyjo&M`NldTXa2!w1d~!%-!tZtxAM*0}bJ=(4 zx<)rbh`i%LgPP{O*-iR=Y;3?>P5FMVp$V z+pAKiT|)@(q4`qiZOXca%GUJpoN{(L-=cHn2jz?YOrW^JYyaTnRvlBMAeBS%wpo2x}?R$CZ~<_PL+G3taxs}vk&EvpQzR^i`p>! zK_RhHD_0^Fi4W&dHckQs<{JN|cW%*rXS>kw%fJt!sDv;kp0OG8__cH7LJk^9RD6aPR9kd8DM#xSe|aOfTOmDvRvy77ga@8J{KMpD8uT`LI;d zobK(02FWZpeNF}Zt!c_>7#dGcSh{s#VyV7#Krhw z>|0pUfMgbMCsY4TJ4wG@dJVFUq?)<4J1+fPW$)>H(kW2L+UhfWG}Rr-haPhtqd`zq zjcPN_@;lC?-;V?GX&la#Ze=Q$&X&zoH%a0g3JUMX0w+6c3Gwf*-NE=g`dRgZc5>H3 z?WNuoCeyycjx|24Ug?Cjj8PMgmA<<5GHrxo*W^P zR)NclCCuF0Gj`^7M%(HD$&-9nWIQoc4<+TfUBr< z$I3GmHJ3WvRZ-|w_!gPiW#MZysT!g1;h8{OteY(D_)ijQA^mSA646yu|pp<@%)D;I@KVcB=2%H7Y>* zq?~N>jLWOHr2gUu0k*=S(@#B5Wy6%`CA|#12(cRPw7WNYKtjG-9?b==R%;Rw# zQsZg#yQWZj3%;=wrA9aLp!*cg;+0P=@ zjj9QO3Rj8cTC}I7B4%qH&vq*QgF)ZEg(X@%uSRc0S<}wxd*;|o3L+SIm^XQsqT)OF z+V4wQYIR0FtqC(bOBC-J`pz9wJ1chzO+LvNS)y3sr8qqig8}uqbSAt*gsrP2s-mQqN(jB$=t< z=6!qOj`euzQiDuRu;}VmIhEmftZ%E>SvwY;R@wP0;eO>-xP`5|m&(@s__AE>x`*5b1Qqy2klX^yVWm7a|W@AU>OR{6fZ`j!hm`Tn6ZNlwW25}a^kUWiC+vFapXJ1pk zFMDl$JXk2NB?PG!j=7m~PZx}@2jF(GaL(GOLU;E*k2&6UJ(5I>5Bly4-MjAEUp#n| zfKsgV?mMlS5-`EE7gu5S&5s|vX3JJSvVR#%@V0A_=^u2oI+BTJ?tGTMIawf2D(Zk6 zdQ9{tP7>#1Vx6cc84JcR40p5rv>=;U?(`hl&ayggE*)h@T?%<)MH$D$55T@5Cce7mg@95{bdn#pRGuU~^m{LU{Z3hgy3lC@-M!7mA2ff}r zLTR^tXdRT;v% zpMfjc{*jC)jrP;~;Z;TxBo_l%%+`J!6E7du**fw~1w~N4T8u?wtVwxte%CJ?Bg3HCa_m&?-)|zJYOZtSd+B~kB&=|; zB`O~iUXuI`*3R+qJn*w)p7@qFdAgiwWn{N^@oUO_|2ylb7VG{b&}vwZF)RFTDBo6- zyf4HmT~mh4O)WGZcd3cX0XnctL}O2Qov#d$B!uHz2GkfDco{VL^&V!w25gen%bTNrQzC`Z%!4Xr90*3 z8iT08gePndeU+I$oNhmlCZ!Dl`W=wG41~P3lA4@W=^J#)9eUc$$2sQHx!FnH-e{GH zX%!@S_J^V;SIKI>_;Ze{%hLsx8!=M3Z5gus1Pw-~8jJ2o!?`DM_tCs@>zM4^e&TA` zReMJ%wAnWsfDh87f>_t&qrvKQgPh=Fh(_l&&edG|eB&z1x9osjT{$_2vm0y!Y{n?d zO{!rJe%~=uX3+fu6`ribdWw<=3Y+WUY~s?(wW20&JS4FNGw>`|ukE~_qE#cpO3Dq( zNX}t&A6U{_q1+z2?68+R={u-mIaTrvWF42t3dV5$b>0sS+L5DPYEt%1;ds6smiCZw z3cUgYWA%z8u!7zaB$|J$_uMYW-c#XSU2kK}(0^J8-fdPO-Xqm?Uq;8M`2te# zRHiX$n4jx!YY1R966w5`OHK&m>_H7qBK%UvtYNtWE zucrj%vPFWVRCqFA#C+QhwUn+N7*MIn%oezSGqg(~-E?sX4@M@U)8b#;HQ#wk{`)0imXM~R1` zJz4&17K=Q9{G2os*A+wI?dZ;;+p;0R)yHW0KT)IlFA*L4(-f z!?jDB(DnSvB7DoVgrJO7DfPfzRsnAmIUZ1>xchPqoHPBYod|uXv>IHF7F=pv7C$_B zkyN2aAINlkDj-@_25tN7iIjLzKe!#@W0ahpPQ`8D{%sW+fRPW^KfkDa95o9H0=)gTk({GFDjH*6I@kLD%1Gtt*oAN%{mB( zE2dkriLdL3xYwSOmxQ5Lt*cax^)`tY>RdzE>%Jab&*@cs{rnu>u2dzKSL4;IUh#oi zQOL=?Pi*o1!HF@RnnXA;^Q#8=;`t&Pf)?4`Ky4+zJzYF3`7hz?6ewuzODdF|A;MF) zM>2oTwuNkx;LMJ_^n=A{MA$NY=Ih=JaOISoRh6)i`f_udDXpBEZNCLGRugdRmWV``Q~iN?!ik+NA0cC3^* z0MA@6xJJZ@2)Y&L)Tj1oPUe4?cex*C?dNd4ZNnNDW^prO*x=3kg2tK59nF&<-gVjh zSe&u!j6dfw+xEJ_a=UC;C~=cjFq-(f>|Q(l7F6jsMd({- zT2`*-vvIG01NTBZ^`+U-zq(qqy~DuoQMA2Kc0fJJaQkHq@VvoxhW0=C4qeKUT_e_4 z@ZHrBbRc#4S&(|xa11}+TnrzeI`65d=e&_?-k}G7r&0cK=RPYDgjL8Loq7(?Yk%V1 zk15*6;C%Z4pz#nhW@3w4wlQvT$bE|%?3BoPg2L8@V~Nat=hSA3pw&Gq#YT39F3Hj7*HV*K(#33q5aW_4W1kW8U75xU*s?d zwSOqX>Z!z_z9zjyR;#7>79-%AcGTGDYYwyowg-msBx0YIOhDr;LUOdR_K!v77wmDM zUnOQOQf?;8M?KfU`xdz^-Fu^X8eZ)~=U}g~E{~8MUfo99ICBHfrE5w7-JxQU{+qdC z-P%xHH-9M6tpvRrcR&fw64+Po6MZ?h~UnA1`@ zH`zy|_hsd%HWC7>B$AwsI=>5RlqJw)EdM-Z?H225vMz{|SW1EhagF2RnX=5j**zr$ z$-7dqvu}D9Qy{jE)XT5TQTBV)QE#6jfJpf9<}eeV&(7^=o^<`;LYD-f0CX=7K?u*z z`O39{ip1L26pS0G->8IECEbE?&2^2uRBf8%t-zq$z8Vo$RD?iVKKA{X0PTIgOrO-u zP{J}#Hyg^8pE{=-9`_P;aX3?t$S2`EaMQ0$mLk|b^b=FOY`YH}pNQ{H>G_yI(n)b| ziU?q$@0v34vsZSf!0%P^^cZY~z$21$8x)Ra0o%7J#3`p1R#{}4m)fAOkEm{+E+k4< z0{KoBN7;^+Xkb*T*fX}ieP$X9LDr~>Vc+nzO)D`JA>e=h3t-jDJ{kGNB|d%ffajnj z)1m=ezSn;RmSG|AcA`xjq_^T;qFiSD+_G4}LFB%)?ZK%=x{e=k4%F#u5Am+G-11@~ z9;4OMWWX);W}6xIVTxxqRImaIw#r6$FkUzFS*-JdM=tY(=r{9ABOl{j4@i))SWY@h zh$Ua$_e}W|tV@jBYS~Yk03iS9vZ*Dpwm+UHIe8^Tj?(;i`Na;sY1dZ50ET34 zx4(Mj1!+`FJ<>0FN;IfPafT-`J_u9+(u%AX*oNejuv)Tr+Bvu1-M(g9yCM>WZw>^u z$4 zfs~1~hB7a6mHBnnTH0jSjRXeZ1^cPC61#m(ODV$9s3Ou{pc&JwuT1K6`3dl@Qls*F zsMSZyiL#uQq}ObF$0*l0XE{E%(v6;r>`<+_C~naAr7|ouJRx)z#KnR~HX~^5ug+LY z;oNteCF+iGzfWC@`*s<1m&Fj{Xw#nfVDbGXT}=R=&Y*d@8sgKC$?yp!$c(arWcbb{ zCD;4ehXSji<0h#1@Q(}P81eM0KEU#wNxYAvYNPEXMm>YNx0sjGE{)rwXFfO$CmzQ@ z!hdYtNArPMipQ3<$$m$w0$MD^u8=}Ql5yhD$=U7JJpsoVX5xl zjX;?1gf;BxhQZ9lM(9I@!^F#Vvsy?D{=r2>bq7yABH`s~yPcqh&F=hP5cXCyk z&}%26&TCs`LJ;A)j`fs>+~w*MZqty!&(tLz9!A*Z?wA93w({NzDd$jd9&l~(+nnIJ z&VegiiRN?eS!$G7rb(1VbeKCcX#AsC7RgI&jKy|z`&uY+FO!BY&&?!U6Ug|sq+ReP zHOdr3f|Ykk`Gew56fzTZu^J6c2*sN_Snxj7K^r&a=YCSiBaQdy{Ov(2IYMTzQgyAwKuVhg+TC=J)zt8ZI#?a#cs9+ zMiI1BJ3rHzh@LH_y!<}Jv$imz{#kRf(L`3(dHI<-=Vv9m=&r#ce zJyv|_#Tk7y`P@EN8a&qcY8+V7%p2AAuPMz3yM^QF)bcq5xL{ndP2GoG$9x0L=Wq;9 z%jvVuiu$yYnhrbNiqGytX%>aqyAQSG!N`VUhP)b6Xe4UvoEC zt0`sV16xxu|0518U->q^cpJkh-0#*IeO7G_?wlL3*!xS? z7hDh#^u#{3!T77Na2(`IS?;IVRGD^}K_J0-sn5JES6qPqcuEj~RV>*Pi#1HQwm_6P z$RBc8Q}Anxbkxb_e_#vj>oLh;^dUJCgalpDz0lv`LMSt@+J+L;xYx1p9f$(>{5^xyTZ}ukN;#j}|efcXw2#by9*1T!s(~pInRGdIw7! zn~(MFJ8B1{KV2mnuN5+=gYKVm3ED)C7!mMZb#NGTdL}642oE0q%*I}fYSbQ^{Dhl8 z{v7ULXNbhB2;L8}12uDI8B#-Ef|09ED;s|`>r_V=yYmjFwMiq+4Td%3_&-HAOj(Fw zf10x}FWO_W?| zL$StbDasY!agl}VyuilAahFJ_ZF|pwEt9GQEp<9DMK8-ULcj=Qs~H?dS@U+!KXXAk z&j#FR4%34k#AXhVw!?UPK1Hds59&7Ac1BH!p?=pAT!(^+GE_WmS2RrQe-z8I*lR>Y zzpLeD5IC=()G<=TVGXLaOINwOm?uq;^CmHMjTSsxytyM-vqGleHzPw!<}wK#l|I#x zXt0}6D1H_&PPvEfp+R1(U7M7ifkI&dld)xwGE_>*6at}IDa;haJT$N9o|it|OC-T``1C;JfF zHttp#_JE*ZtO0Is+|0>C@3qawnH&yLG4R9{9t58zN7ST+?Ibo1G&sUFQ$Zr>Fw=7y1cjxs$MSKqj8 zt3cgGxV-3lP>CkJJSsz|CbbW0H<{9KFppYUK;Bi&$s765P*n=L{6l7Z>bl&(XdOz9 zo=1XcFv-*|+h9nKdw9XwCr;+nah=y5TYFMo789KX^lBLrxKnrt>{G3Rjx&3F4{IZI zD)1G0{W+WJqA=0}E#*=IdXUW3CuNc8*oKYQR(C}E#cBX?f{cSAA>an}tUKiiTy>3R zyK$y#h48r~;(RE$ZZo=09fzJOMnXmJI5oHyWu5S>wPYZ6JZpAF70lhs)WP)*%45;i z0qbS10+{*_xb-}Fg}9cN=OKVO1?mSnMFamZoc2eYJ>>{PynLK4)W99RErYz~&H9dE zok3lmiWhoE_#FhQ2P__T+HTY|o`Bpc4Dal7O)2~+eB#yAnZWuWzp*G27a}$VGX9z~ zjefT5@!mkviJ1tHLDQ;_QJi{h+T#?aBHtjv6Xh>Oy%sVCIw*9i|3 zs`VYo67)E}JiE$F^k+fv)OsF^PG~Xxb*ehrC*ETRkk#Zg_1b9{&E4Wi4!%WY1PS+NVRM(JDLg(`go@*ip2wwGc~)Y6UR)$1@bur^4utvQF{PI;I3?>>wU7FsnYenH zL6X}@`nh0R0}?uFZ>X>!t6>>h;c*q^=ABwEo+mV}dD6&mc1!r;PXQ;ZZZ~y{sWC77dzeFk4UTl0mQc=XkE?IZtBX=Qy+9}T)7}N2Y{&-W42!*VKZ2j4 zaZIOums9}r9qqjc{yMJ2)u>&}WuHW{;>!Hk+&#V?Pkdp{{V&%IHUZwGO+n5jq z_o-@Bu}WiND{xCI_O(zF~mWM42?Uhru*@geCVc0CSI z9j@gdZ}pt3)|7fIUBDEWHE8j7DE7jL_-r*paURIdJ9mJrT7(L!n7#Ban@2`#tO)F( z#;u%n`!kOIRGzTET&Fw8bhjGBjVhaxWG86JsTykJy-}gpJmrIE(&wG$=m^PmBwTL# zKmdN&j%XV-3srd?zkt_*M&jRzj<`)M%T3Dl@&YYj>{0oQpd+;fEObifbM#BlL&^m9 zm~du%z0yb27DangM0%1GV!=r}Bm2AEwssxsf~A+lFf5jmxq0Kdlp;spgLQ1WF=I`d zXxC(BT+lsq7DyAQoi6yJU_^I#L|{!3X{7Gx$VfuxAdp+N>%M9QgtYz+=s}Z@e%|)# zT0^V{wGvcci2-g9qnOTWdxXnTLVJ<)j>6|R44q%d3SlO|sN@}iN$8THuCqnfzJ zs92FW&ktl)=NRYrjTfVj4a=kPNm`_~Os7N_-fX89pe+{9jouaxb zE=uC^sK{lW%y2hNXG4enl<#A;@j=KIrg54iji-8b%da-~V^6A;%5=fFrN`ZjNJ#c! z7blAO9$JzhE|-u|zyrd5?ajsZL_2iF>(t}+OFU}W@wdm_A0Hog>HW*ByJ8?KRfBFV zEs5$AbhC)Y!g|f7^}McGrD%fff|cXp!1h5)2&%ZbMXN5)nn$o%wN{RJERRqFcHS>j z46-;sH|KT4-1h(mbkB3uNMiZffNNs*@A}k%>aw_j>!(h;5*wLjPk*6tgx+9m3cfV@ zd<-n_CcZ%CHjhat2w5FY`@TB7uZ`<_Nx$B#wv#BxVt4yyN3Q$9TA>#9jrFkI>pW!) z2$sKo{IG^MxQVyjZ^LU)QoA9^n03?VY$9=>j<&A*hnxcaX@d%Hm|q7war#k!+zF*N zj3z_vPvtIif49~|NRn$=;4KmXGCNUEI&5-5**z8^U<6|uYyMHc1wQ?_J$0pD3*!FN zQ|k?xic}_v$4emW2xve+Mqjr+o2B`~=y8a87uuumf>O~OJKwxc#4{|}4v+l&VH#Vs zqU)-w8X9&^8T{nYc&3TCnbe3~2%)W~7FqgPho~fSiK(+1mFjQWIwbDV9G-%)zWAC2~%UM;DwxdN5tnjKP$b#B$=+FKv7yo0+DK z9jG-qn2)bK`qA=JMa^u8k8r6jGO69}K`S#~wy^^Cqxg z?pW&d7+mai#U*9sYI2G84FU(AhPwLFY_h3vIg8()gUV>7E#~5as*QVr7}<=M%Mzsp zwS6e$FcI5PudC~ZrL$uR__Sc}b5~!dL)~T!BLgXcwa!@joFT>JASrz*1=4bmP}GMg zxf{VN#~Ja;A4YsEc+}{m8P@1`C6XYWX~>IhTysdm8?&;OgmT`%m@K{)8IqQ;AV2h>UKr<%B-Q*ZuM!{WMHAr)e8r{m(hBy)Wh+Yj z2L-jw0W)b`AL_wC9X%zjrc@5iMVn-gSc`VfhBaG&|)*6H38{7N*5^w&&|qr@`K;w?VP&g&f0dDkp=XyIf}kH=L}_*JE72YK`aGr zxU#x%Bl|OcUJ@|1ig6fu|9Ki6v*!>iG53`Cs*a1q#7^9o6y0$0ha(4s%}l|`3GBSD z`*i`3Jx<~B6%L(XSYx;u>&+Pg(X=d`o^GWim0zDASnoYN65o9z*_a2|t>zP?Or~g5 z7`@m-KrS-Y_B_3`P>P)pObrxTC`EoJ1fxCwE&;YPS^#EtIlHvlSaBI&h%i%y(^1TZ ze!J;EVnyv&!n;KaI)|+4)UChAv;B3iW9~B$iYUkmnT(Z#(>@V7^at*C*~) zBsnXtxXBXj?vUr0q=YQWX*cgrdY=CW*Y<)N5yEi*XX0dD@aBj*mJ*$n|02HQTFnnl zB)U+KMp`E{^BF+)<1VXDDg7LE(~UPwp5jCw2ftZ8VFE}LaEm_*3+a8-j;-iTNfLBV zz9p$k#;g74F>H(cCv?{``4tn+6)6iT$M1bk-eJtW&|XuMZnqtdH7WCDP_`Gv1a&zC zJ*#}Az(l(C@j>o@w^PL@YzW8rXM|4dYxXa}a;Zrcb%7M`9(Y{QkG|@TnD0`bkozbR zI}EeyAMc%!M_+r~2ZVYV{QmDKeM5Q7CT$B9xEK>f2zD_V{vHN&+X3$Qs&K?NO zxouf~W9xirH{kZ4a^`%6p1pZ%n(=y34bOw=Y&4(l2Il71{mP>pfWX;5Ip^t)Hof%g z-t?cE*%?@j39F_oFZzp%n;KZES1bZd2;w^CRRg|+BMguaP$naqc9<$r+SXhqX81j- zd1z+;Tc`AI+}~9Q!X8KJIs1H(m_1EW>1%QH+vc4IO9HrHD9G$$a*uxP zOzIH1oFo=bP&=E;JX=_8NZ)1^Ol0|wzW%SpliEuRlrhGSIYF!#jpkH;>C*qtMJ@tC zE^QHl7jC)i|2m4lR$dHhH5ij|8a0ohjekuO{*#`<((^BZUQbs4kAKK$K)@tqM}lMM zDxORn{?C8@`+Z-$M0~y;EG5QaMJ;vHCHjwY{|l5893s>6Y>PK;BhRV}>tFtR$^-lk z;cKKW_|OJ_R3O$r*M1aAOG63Q9r>}+Kee5UW`fP>>~6RPe^uvy zUId3I1u6Qya6PYa{Kt1dO@`AXU)V3ynEmtD!Ec>o6HDn0%WiJm->bYQp!^3E=vB^J zX=(0nF%hHBAs^{53C)md%zD25(dTsz{E5Rl#9CHoxvj$bkxp-Vra_)}S68`0n>dH+ zld(8+T5h(9xBcI&(IQPmk z=Ej69dEbObzS;LvNx%D+^XiGv#h3Dop)}kCaO}S} zRS#?L)$ECpjR#b*_OOn4KLXR?>3!Zap>~Q`0}l(y6E{nZyLilYJjsI+bR4P7x_AkK zV!~8z^mdPx_U*Xy^zE*2+--(|=VhR;r^RbHq*y|{{aR|y=~miD_^^Qws3*Kc73zxK zj(Zd=`c4QpFA@1Uy@Mvh&?o_?iOi=mn<-StIQEymk%U<4!Y3&G4$Q>oWy;z^?{vsv z%U$RknaezJH$06hsz^W6*lUD9G%TS}h*BASoC9S?UuW(p-I?%63LBt@8iNYIl!yMZ(lJsCPud!U z_;OG^hVW>I4Q(FmizSM-#y4KU3X-Tsoi0JQ))u;fb}32bmvD1-d3_2|E&P3e60`o2 zu;h)KtyDwG=?bbPs`>=DHBcU?n=#scU^>XrH82z8=%YNYYsqYWm<(vYd$`>CR4K}a zE++vuJe|g>E_q`5WO*9qovGjU%j)-5mM5GKI9sLN>v{!;n<|?G;Ij_`1fSBJ_5A@h4ik;^%ftUd-nmjd-$aI+9Y8#5eR2FBqkQ45t6E# zpj!N=*ZhCiziE)*LRdI;i`^>;%9>IX_g0;5lS0cm8xTf#U$%uNTT{@!`PWYVUp*(7 zUg5R*8&BzxTvvRdbPVZF0`v1T7P32+L-F%DgL6e9|J|8cUZ8LhZM#>n+%T-fZJ?ZV z)zS|S`Xtl-0)G(3Zv;+1#!6?ECH(hT^g<<|=z^twk71!B9`i8T@2;oLJ3EAIXS z!cmQ2Q>$9di-zr45WmK6Fh)CxT}ol{rx`x#9%+Sda2YAvyZ?3&sL4I?sYfoJ+9vH8 zL?h@(4-HEvdCd%l-B$#!UgbUGLQU!dTNS;s_7*_6SuAe<<<%)GQLctlk&s`wr*&y5 zO}d{Y>mVMztdVrAMfUV`d4*r*CXI&w8-OG*ermmINv3h-c#?6}?fw}N)%cE;nuvAi zv319={Z1Z^TwKpM#3v(TWmHH`oMtp*kU8ff)w(Cj?f#sW_Fp4G3>}gW_nKn=h+|gy zR->neMub0)M-g^bmuzvxA}!F9+4{to9aTF)Ypi@wZ|ZQm*ogHoKuN|%OITJx*2X8j zf%)SnShoA&Vnq|*52Ev<;4`Ujz3(~ykjcLWU@YGEP2s`eP4r@qA&ij5-$H@_LQ=Sr zc7;|#DV4>1FeYWEP&#RHrBSzVwo+dQ?dF{A_ISY~ENOOjwx6dsR|yLgu0AmQ3>TQTUa-rX89#R!=Fn|MH90&23>i3a`kYrQu%(!bizKrW)VaZKYYu(Gu<&&35Li z=88Q1QOndBjfQ^?C=_;?$jK`yB`U*3-^;+8F+9WV2`Y2-lVC!A z8!k>N&Txz)9wP^qjlo5wmZQ3GI%0T{@rGNWY7Q@Qs~gBhP%UviwUpdo+9a{pH6Mz7 zQ736wA^z}DV1(|kKJh;nLLwMDt|G+5XnSwQzvh=sM5Qx(`QKz{agfoLS>#&)_tb5N ze*211Y0E7Bmao(j9!n*qq9viy*GBXF!CFZ%mo#nrwl=7a`+e-UZ}EEkKUzVJ3zaD@ z^~47+zA#OfTJ9QfAr&V@5@tafwq1>Fzm6GO>q|`!1znhA853a74Jy?Z`DVn?UD`(#q7pJGMqt3qP z9&*X=2;^2AV2|vbi9r~I8D-2AQ<75;Sl%lLi{hoM&Lg^#GC=Omb&wrD69T9MSU$4; zEl2&~-P;n?@!jp{bK+qtsXciZPkf#F7>S>$2jgq= zaU7?8Dg_TEjL+srg<-R#?GFzk+Mg_mgEIfC1pD{QiIOQe0FqE>Z~skd9zQ)+<~Mv~ z+&S?oQEa@?S|T8$ZFd)Ut1)r9JT5;Tib(Qf_!;9WOrJpb!dngMl#0O&`weXXn`n!PlTsM) z#8_WU4#aowtugrXJC|uZ5mFp&MIsnI0wa@ zV9Px(zxB0d-uezNOW7Hg^iF@|+wCHUoF-5qL(nO|p@DDs zw|q(G($dZR%DJYiE3bm8DzBfrg1*^kSB{k4(QcQTWs|(ok&Nn<86Gz!I>uyh=IXV= zx+A))8J0_~$W+puH2PzUpj;f!A@8ugSeaauL@lZ?%J1Z>PbjM;PWygKUh5A*%U3Y_ zddv5LKJf(Y1HyZdCpD(sf!CniXfeOKgSmzET)28&!`~p&SoYItpw@z1tv&_!@>)iM z9QTQJWNajNg6NSKm12-O!aKs}xVw`jDa~Sk<&x|xtZj8^ABJanMiYelx~=<@sR9Zd zO1`*a)tvXK_-HM8Xwbdx>^lw^WX`M1tGdelYR9F)bX&XaycblbNk0QZFb`9poFMEq zR%)n6L3hyV!8DtbRKb1J%{~n2IG_{Q-gd3sO%#S1cAL_37`KLrc~6N`HT~M2%|%HJ z!dF95c#Fw`$oM}WtmTpXfwJoU654&2UXjf&9fW%TPhay-URvgF#)Qq4lAH(BiMM~w z?(`+Sjf&03?@H;TGUBscx|{T@<;xU|i{2{&wm5|l6tx8jf;SP};Xu<;b6fVx)l4!q znM@)Y|NB>2Tn~bL?!VtYo;n3uP#1zlj|i9BJSdzp_1}Rd9z}OgCb&KGm-6VrUMz3B zodGWfgQ2u|gD5*{Op;fn&9@8y*ROfZS1~x}p0Djz>L5X`?YO+QYJgMPWY3yP)a3*L zQ=O2+?Jhm{)zi1c4wA%CFR!kz54*y%w^M$e2>Q$_#(A1B4=*q;sYuyN$)4?AV_h9y zG8^-$0L()njw^B8?%^P1<%(MY~=LaW~m-$ZU+JDhnD0a7UAiFMho_oe39z+`$@NZckZmvgwtG zc%fJ8D^~fj9ajzuhkQ0?xxvu_ms041faAKXVoG&kM~A=dMi0lhwzv| zZJMu#^(8OGfrMjSt-;QAtVGc>M)^w2Ar%or+;c$2PN}I(nly-958e&V3Bq(y2hF zE}IgWYhpgnlPSxgSC2t{6HZ-eDy`_QWtt@_Cxqno+HSeA!Y9A)F9&}$Sd3^NO?HVz zlX6F$S8gwwHiT9C%{-zF4(JeVrRyCz{T zgvZveu(Pnq1<$PO-Q{#QyF91<{2H))_fAt}uM{!t)e&P)+2xGt2sXtk1;DT=bATtI z%CJ3EoKyFne#0$A1MDa&X=VZ{2;%^>dD)#ef58b$0q9GXw?4lYU9lKFC}82||Mfe6 zV#dl}5$JicS;r?528pheP&?a|iwpchv0XEeXGyirv}T=r*HfO_D3}31TS%@X$M^fk zX##8n%Nl##?s$RA#0nRG=cKFPmr6!KTUIn)`Mrun^; zc}4G{AK&+>sM&f-bz~{8F6Z&)#PRWN!pbOL7%TbWvDVBN{48Fg%`si7Sb}>)05mVI z)wH^v&sxn1xcRlfTb4PPU_N*`C zkS?piL}KPBmdR2&vGnn6rT5S{qH)PiaXkR2o_xwui03Gl9q{9;n(`pEF>mf z-~Xx@Y>F&4rkY99tCI>n&xQ$3ifc;J3lYCGd+5Dc7i%w5ga~k{GrleE$X^}JPUW}5 zJsnzBgh6i2(d9#2-eT_&;ik^jrJPH3KL_F}Km1`c+Dnjvw}Rr>Com=smB`oh`kBJ7 z=Udc)_b$G>jm-~6Jn8EJ&!C3*ERGhR=in?7!F5+W!>jh|v8Oi^q7XWXjwtCmzrT2wloaP%O!E&Hu>6 z7(*w-x4yHhVQuv={K2y6H>oGn7O(|6OyR>c={dZbB?IV!k~i9?_2L#eiXOfWUk}`u z5`mEO7k&NB;``Y8Tqk9Ls+`F+?i_g9*nDsa5+50<)i_8{?JnFs zVF)qDgp|wE{Gi0<|825*H%*;ttHrC4f1k^4;$%up)1jM#c7KIjI~ZMupDmj-RuA0# zD0T6@-a{HKyZPs5A4U8+3rIzBSo(!g7bd8RWOG7KQ3tkMPCY<{&#-ge^%wcfV`;$HEOz4=MS!?WmWW58$Fbn#IVTL6 z3IsLaDycreX zlu%HO!9M6YW0+T5dujv#P0(wwmRvA&Bl9>G2s>AeS z7@LPr;hm(C+BGfewwZCjj=zOR^;@im(6p+msog z{56zt7z`3sY~FC?&jPIrH`vTr>UCOD@tZOm27Jk>&MK=;Hay+DA1JO>&S0AE`y!Dl zrirjjb%7lD=`A`gWp3KW_}-y*z$%^Ob!sCLv(f@*)&UK8i zPNS)qo)GqOVmR(xvbyt;axD;&GpbhAd8yKNI}aS4&bB^-1VibJM0keTsd`6LxmG?g z0%m%Pf4J?9mJp~4B^!HBkBPFJ|I$~7-a+fF$8{gunV@*RNd~#*hzeuv2uyvhHq@wE~EUSgR z*dXqF+z_RV4L3)FQsw3>>rT=c@nI_xaLNg2+#ES%K<2@zV230&UD&U1v2EV!|9Srl53gEFHi!hbT2Dnm>8FGQRLVX1HEnDV4MB0Tsc>u>-Qe+H zO&W&)=O510Q27(OWDrJfB;PRKzn4${+fZ$Dg50X7K3gqm@;IT5fLUF^re=Z1gY-F- zd3cu9C9NPzxh4OZ?%lU!odNwU^(XtQR(qE>2`?1PnH7kS6-rB;D+qO@FILR6$T&4f z91RQ%l)l*KPd`p@Y(C+ES}tkEMhwq7k3G}*PeFXA?O&kIPr&*VjI;9e3u-ESI-k*& zxsdgzRf8@ow>uX}KMY;px)iXMB&=qu-+QjtK3(|Qk6geVLzz?}dERMpG$xsBw)g{D zwWzhZ`)ade-2>a3+KCtI>?ng{oa^Bh$4yxSuiVyKyLm=pC&4<#zR65VloRzlU2vx_ z7H+gHxGLWhNSFGd{)^zmS|>1)x^4T-(P}F}(v>!$OL=Ce83LGtHS~C2o ztZ#GL7VQp|UKjd${Y56pNpsMPI{30~|D_G|^=4W>aBM5H?1IYvK97W$I30PyvkIQT zS@M6E`0zHG>xuD`f0xA6UA_?7@h)PUVjrnXz4w%AJv+Up`gP!D0)M9CyO}C#W&OG< zmUN48e=~t1#tRCahc3(NU1`bKh`cec{ETZh>+LfG+m#3!@oO;x#~`TywWvFni7(39 zyh|~wpx!Q&)#oiE8yolkSA zcHwEAJw8|lyr`5xih*7+elR)`RwKRJhc(U*E?G1~3XsdW^rW;mfMi^Q9G;5(xs$Ah zpv&y7hm*lp{_F;Q4#xJD89kV_E;CH^=NOhno08q^etlwWLU=UdpRFbeC%cWk)4~mh z6|=$$C@N-tjUov@uhx&1=k~q~2%<;L=ub|(uH#ro&=xvdZA@sY%y3#CHpkYbA+3gu znJlS?H;p|Qd3P@9A#dOa?5`yxIaopnNl4PqX(P@9#)|7?;@$ViN-k{i_ksOUH23J6 z_I<9|1wPkH1WZ(N5*&t(T?GlM;=$7oN%LEja@t;@OaaCT#>LaSdw?GWpBLgC_)&jf zi1vPSnNeQ2yeSQKpTTLqTCNlvy`fWzM2d6GE{p$-zC`7<`tH2@`BkNbYR?{_atH0pBlLQDbj_@B@mp`-!|wC6G2?DXAobd9 z^PrD;&Y|T;wQB0M`)ILp8(P)IN8bVP&7vkC1IM153Tr&36XL3DT!dZ%Gs7QC<#OrZ#n|MxbP8d9T7P{3;@ck8LBDyqp$f&D}^w-eP(tW?GC zgCLIY`3FukRryXS_C<-Lp829zVjfCSg$4paV(P23DI?S7>61Es-bt9q z`sKDpXCQ$~`!9)!i8;coucINr&rPSxYAf>`qg_mkYIlC?%{xvYP-?TIZa*X%d;665 zh4pP1)gIa!%LO{Mp3_F{$Rer48EGJCSJoNyIDpUV%-{DSPtHbU!jg=4kA*%aTH_J{ z)(dO^e($MX2mFV)m5F`34OS z%r7G4In(Fv%N|FabEbc%b=DGi#{K7G>Zo5Sba!0edr4Iyn}23`7Moo3t4{dNq}Yjf z`WPVOVPR%srqDQ3ibBwlPtk?5_1>WE+Hw6RnM(NH@9c|CbatAg*^(?iul;7}yPmx^ z)(*U*2hsjLL7jD3r_O8YfPP-js*bx@_vvqg*Z5`F{h+54 z!|40Ph1Q^I=xK?TC#TzDO2ZGiux}ex_1`x(6Na6(g(0KH7a3k{q$Tprj)va!Mfled z+hZBcS9p06=MDb(OM@stAdShyK77fM9%_D&+tu-)`sQ{&(BucnQ!m+MwIGdktz`tk zo#OjH7Sb7uK6Pfh+rN#X2kAw%N(Hma&^g)ol>+G@H7djjOAfHU~Z*Es26--D>D( zs>S{nh(cIA)0)<)(@GHMx9Wb-liY;wKo1`Y|L8#8Q4*&JoW7TiPsT zR&5cHd-JOK`jU5dmc;rv@HX}I*oIM5cQ3DMnzcs|Lz{|BPYUA~03#7$dtGPEJFST(~@J89MP7-uxM+GcRi z{)O#5qTijq-zX_ogaVtZ8Nqj1- z)xkn~Bpc(gezndF%?mi_6F2HM(9#$f2^h_uF&Odm9wry)`@%kbRq(w_ghw= zl0SXu5~LgEWGRBIf0U06Y)Efw{#w9s@}5V(PC9rJU`wy;oUB;pF)$kmLC(Zc@hAYu(vMnZQMtDIQ^YIZ3vU| zKH(iKH96-nfulUhUpOwkweW1M4CM=1%Qkd+gY4e|p))fQI2GZuIhUdHaE=mEOwRmn z0*e!=K{xiom`9H%qWGS&$k{xj5xZy@Si^3A)&vVxR_va)A1;($-=pXFwZvBPbSIBl zzb{l%JByC+3PBpzlN)s(Y2CGd*5>(%lC?@8jKh9UuMZcKlKwXdaeo5omIMM~MchL2 z5J_;yQu}9%sgif=4CpXc%R!u@m(Fp?&ca9B<+ARt`g=o7mah)q84p|76Lny|d<+qN zL}SKumnL@T#r#Z+6`A!ES!MH9;va@VX#_BXH%2Bojf`NHKv2^j;S6v_K&^VOHgOQl z{_%v5^+N2RvM$e_|8$#kg8g7?%(A3vv?S{1oco@2zGtPrX5l#^&sl);hPmT$D}4*# zU0Y2}kR_+Cin_};K2tTF)laAnIKDNE;N%uipOk|Tc3iSZA#EhW$Nlw4noRx2?@i}a z>S!gXAwrxT_T-4i$ReH@^4rRSEzI^Ifi5&A6uTJ3s!Aw+ZLc}n4(e_eYPSxCY;pDH zZPz6n+i|$!yL4h2>+mM!y4AxGKB62(4&95_lwf1sr{$9R9>k&?L{>FKiawom6wLfw z#gbaE>zFL7p0uW7ML_uN`noOnHwn7jPxTHrz&n=gq%*zrkx_Ov3 z80OGZv5I|@E5O0)eLgDfSt<=wAuQ0B^znH}98we|u3nRUKg7xxD6cwG)sADV`GF21 zjsyOrZt3<>?q?MNBRC-47%kdowR#2YP_van5=vdGk4`r}if_e;v~lra>brRBIV+V| ziRM|G2@FGvq|`uUd(Nq>-du_E;sp<`S5~{>5OQm~@=93yxI;7Nl`>S(@9uIsyUL@b zfjKngPB*)JnKH_|!9KOo#@K=NZ~H8`ACPpbf{VmXam{RE#Vmz1^SwQxU40sQ_3>b- zwEAz<5EqiE0hlx-l2ZLLaWNuz`j2xjTGJO%MOw!1vNvDz;hKf zMH;Gc6WfG7%@+Un3ML#GaW!IY;FL6zMRzX&pG2nLuQ9)hYUVKF{ezZ)ZqzL`zZ9w! z22klmgwK-vCsd%?S2McB*1l6yyWfe6C<-ddCE6XP_VvXs1HpZNEcfKb$h939U#7mz z-B2!~XRN83|NDv_-d4B(3q4n_M9Wmoo#gcVU9;4C18J?%5dmA)W=2h0Cm#8DGsoEN zk#XqQu4xLZ55G5T9Dc0J=nU0sAwk}}@EdOrz|Mx9}WtDQLI-@`>3t|Va2`e&7D$#?BF0O(QjaUH|R+~Em$Ncst_i^ zvac{D*eC9jaCb-{W?a+_5*G#;eKOTNvX@cZa8!%YpJ6UIAh=oL60VKLK2fB$q2GK3 zc>{%A5493y`X0A`$u2t9xlR$T>#EQKzA>;KFgIi7IE6s4uf>H~Lpe0V7XOtpLG%?{ z0aj;VJse;mi)^70qky{yyerDvRHfq}qu2RpMHoQS6qT64Q;SM*CPA3D?k~SQBsEL( zIy5TYLm^%e)K&N9bj3m*_~uDYm~hb9yIf|k19>X*C9 zogb`X*B0>&lV5R9#S18}*Xpq{_#DumYTn7HWb;k~zH#t_@I|JR37?eKF@zgqL}Pq5 zTmalBZ-4r06-RS$O0k0mz7Odv2N@;#rd}nH)oY6}XNeF11)w}#JTrI)uK}R%&Lg?2 zi65G5BJTU62s~u_-sc({^&VDNOUN$huLIEOYZ&X~BOHv=8UQ>ooP-=76J*Wl~K z>1TxS&_ zF)`*Fi7O7y7O#a1!hK%_QNIv=eV3BK2gW4Qen}Qzt^pv6QSM}m<$(Z0T@spipg8Fw zC8k=DUkfxFr2^ZVVvKtiNYq`DR!&D!1Lx6}!G}vQK^f>)0|4hOsReGkf>_C@#LI3m z`ZLbEl|+G`ZK}$3G1M;}CXB;xCZ3>;No{e#KaA(&UBuZ=(?rGnL6-KJ0_OGR-4@13 z9`cW$3bPb|;uPX!1mAFH{e3k!Yd4DH%b;tdKE`R?I6J$l_?%?pkD(2RPN7)r{Li}5 zy&zHN;DY00JT_W93xdcHj&L5u0fx)q&6$MlQXXTQ^L_faa%Ijp28`;$zKcaMBtX3;vD#QKmOHM?nEE`xJQwNCsfMFyWZkX(&ll!A8lgpk@ zMSTS@Hz_7+0GXcLwmrCWG59hQe)Q?o7kDZj7&_N#VV7P#(X<-i(R@1IzI!Y};|SRr zUV&AAt8ysGo4FG@ac_a)8-DAb3-3s&(n(wKWZ?-uX?Iw9SZ-QTyTi3S5eQeC?j0bm z;3Qn3)71AgsTOcRJu-G^bgA*|o5T9Bt~g$<)H3Tqsj!mwJ2xk9c6S6^=?fjf z?=CUUW-71s1{@zOC`?e%)z9N(ud!&2Rr92}_R+(Q# z4HDJAJK7XLT>^p1Y=0Yaj$#lT5k^Kv%A%hCwV$d6L^)cG$0s6^{DxVZXsjl0QM4-> zJ)Rb!16q*=No1?k?Uq?Yb3-@VV(d++o9p8Jn}fZY>w=A1J#-|x&h_`KNLD^4rwFlajV>aNZFd z#mqkL`9d5LVbpx7T)|SI)4?kJTnt{jm6p<$nPatu5rb>YqXMB6U=(=K{L^(AMoz>6r^*1D#dFA3E;f? zCcmxrCFav3zbl>}Z^(Ny^lSV5Bw8%HCAQ@)%UYcp$Ira#Ni3K@IlIZw4 zem<9G0U&s`LT50Y7*gg?#@;K>XNX?5;H!s}xqnOO?n|f3WtYR5jbaC)?4gED&SLa9 z(L(eUvloZXyE0jWqPBk%sl1!tLf$Ad?)+#v4(YGrX#e6EN!fDwd9 zmr%us*#U}U-sch{;5(t>g%x4FyQw$~sJv1FIes#MAH8pO=hXr_1 zl9*1qaM*Tr?wgB^Qb*)VI;)J&@`#C(!8w=!g@QvM3fR1id1JKq#4>P*zEzF8RF$yNSwoXxM*IIq;$vx}0?+#%Pusl1)d%T*gh zI$l1747Un|O-GHm)TZum6n80Q9S(lZ4~pziazAWfinr-!Jz6 zB6_mx;4)uKF%dt*P0M1>+c5Dhr-dFYK7o@*@uTnKA5!_jhFKfgzH;wmpm?_UJ4k>q zS*3duwmjH~i2q|T@cz>cNPHAOv`GNCs&CI*seh@RkJ7uH$BSoAuKaFaeERr*IVQi( zrhQtK4Vi$(^C-ccE{7|LVU@ueNB# zwq}A5ezl10Wijcp|Kmyjr_YaKq5!53ZEC9jdmM|s^jQ&cG*?$sBmehQ{ZDsP_=WHJ zoS`2v<-7j7`+sl=A_#>Z{M#QNQ{VmL+j9T%ZC{$mI|)bAVgM#?od0pK|JiCA_2cuC z9vkA&JgDsdo(IY_mPPMWq^<~Sc904|bW!6!|Cjm5En?D8+TaJj@_?euV(Dj(0j2-8 z0$-_oW|%f*umY3%{x6#RpOHxu`nJitM=(C(fA??W(ok;Ghu^bEk5&`ni2q+|fcj#D z3K~DnD(20S^Ys5-YSD9aCn>q}?;7I&<7h*Tg!eVpdRQFIap=^Cf0;HHLzX*qJS|)M zUxt9Ij)o$|};f^U(;v%&i`S~1s3-}=gFAs0~VP&(}$&@%q_FpEbhS?^0_ox5PXVDgvo6$Cp zHRf^e?1v2BGpDqF5t$vDuP$MHMBL(~v%Zds#%mLk)d?8EQxY-zijj%FqJOCGKX=YB z8~TeEX%I5S6m$n^PK8$7{5NNq&u632sv9d$#|G!Fin8jeH`7SYFze5-wk0g)nn-G7 z?F1iF7F#B>d?xi*^v$ zWggiLw)5eM4N7OH1%8bNovT{m{k4sJQ}EkvfVXyYlvB9v)(vG(U)Q%_nwGtq;;N%D z*=rGmH+fPBFr=vHI_>YaZK$bfOQpih)SWbF^SI^Ntm0?qv){KJtS(`nI0VR~Ckk(F z`!gh!B(}tT^9>SAW5fg}GY#YYUga85(ZM&nj^uq$zSg&4NyCU5O6mU|$OBr+v({4~ zJLkEBhPS-ByUyt^3*^w+g^1%FYBIiZkYDLY%Ir~e24z>py_enta}@; z+PQs-5o1zJnG!1_>S=K$k=wc;d598bu^H!3*6v(`x|>@{^hdH5J9NHhc6V-$$J9_{ z)qCl#U$F=Q0L4{i%@5z+xJ1Rr$CF?vsj5C{KAXQPS2D}=B!*oAd?@Q=GHapwgQ-zC#vRYb|B{Sc)(AnARnRAqYtgOAFS#Sq;rZYl_~jJ~ z(vxs{3hmA|?iZ;H0wyPvkX#-zn>8NMvYs@Acc#SF=-()B@k#Qoi&ymv@RP z-CN9P`tf~75}0O{@3tK|M#(yeZps)gj*T0pa;k2RAVBtRa#?KXdP5L|iDyhU@4`qZ)Vjp3&x zv96u)&vdiVB?+v2+vHK`uYKy9g>EP)7R&SSTLGu#xD0>%kU>G3!^3a@Pjbu6 zsX*?}E@FdGqevl}L1l-#{zn;}HvV5^Slzb|7%-7y<21c|_(;}u>d0r1S{$KP663$Z zw-e^B zNv#5SL2!3YyTv;w8rCbtG+y7!(=GW#18|W&SRs`=9egm>2m>KT_J4FRR#>Qqgobjs zoxgs98kXe3P{x4rhJ(fTq>(?;iGJRPKH2x|z3X?b?A`F`!J}uQwoM25eA5pfKHNyq zxmAc1JdEO#S!t_2;rjCHd{}&sxjomIILN!~bctBFRnj!@e7b{36n9y0EVB>dw3-al zcfWDIT)c59vZ}S=gpAT!&Q#tPfoLlqA%_^!GP}bC0ven){=if3&N-gxaoSv0#LWw) zqX?6-d#FV|Sw33J61!)CfbATgOf3vo^e8PJnd)S;*M||Y=u6ZEJ#R-2m+CbSMPhK! zPpJMCW!a1nmAwDfR3ham*}!2}{V~o0(9Fy%RjX7y z@KN@J)#UKlkf5}IV)|?Uz(+X-8P4=WQWnax!GrVpu5~CJLl<8D;T~!;joJ4oa=zq9 zVAYfA*DT5U(Gf6GqP3uYb-H8T8T5mYf9A`+OMatVBiyIG2}GR9p>h6XW#gWYq59c*7>JLY3j?G%?|wCGgWt1Vzit z(0BbIxy;hxK#uO(#m>R8QQ2RbRW&H+PwsL4D#ht5@cgTzZ;_)2DqLgJd;+$Fb4If^ ztk-(n=}c-31-3*1I1i4IA`3Rwp(Wqe1ApuN^T$3k%j4=pPzqv}oUYJBl~nF88#87Z zIjBd!XVQ95`qHi(qGtYZNP#aZ(ad;XWwEFrvjCal?b}Vp5xp3u)nHw}(G~J$%lsgR zPD4XOruAar!Xx@5m&ss=Sdv)(oD`Lny87aBY=CDh_NPY*x#zZQ!}j4zi*GTRfIXUW=|h0%u7Kqv_L!mD?@|gFovkC+dQ6^gKoeIIY)J zz){~nV0@$3uCTBk7s^@eC^3E5zJ$L{w2QxKa ztfe}2oJ@^G00bDbE&MDVVN9pmwt$*!P*1Jp0rRgT`JFdK4LnccwWLuQ3&ylKC7 zgQR!espWG%OHMtnZ<09z z6G9CF$yIlL8|A(onLZ^E?^?a?HJ{xEI*px74PPK{Qp~bWf2<1GKD)Evc_!3+edxU& zpO~?n%w#p7pvlWmA4VZRrSD9#Gu@J`e>rqC-UM;fZZM6l9~$WE%aI2*f6VZ{g=jZk zGGwe?61(lUx1wI~ONAhC&1^^If%P9L4oh-mW0kq(qj*4Y1^II-m45E@jckm)0xeX8 zdHJFt@0G6n`vkew(Tt9tb^DPjX=JYaiHQa>Rjrv~k;0d}0GDlvs?wKCU$}r5B<3sb zCz~dQuJe5}RDO4EBtwbS@7j*KbcFLHU$-=CRlA>&!_(_iwQDRb`c~uvXiQcLpP0JE zlBGpA=__oio=&ySf5X_q*p{(BReOcA@%OI6Uq{s9DgHElVVlqrJ@1JW@b|Sha_Yb6rch&t>I>$iIPqDqdgRTQLpWN&s1a-Vw&I14$vyY zytzE#v0w1IDRdtau8uQUQ|pxvzB-}kt$HIxitLwVP|P0^N&Oo8IAxTnsTHwh+cJb_ zUz#T||LmtrYvZ&x9_Z0T2S&cME6ZMy6I4Sft1xgkD_B(T^VOP3@F0P*pq!A~@#4Ul zQr^#lPs=zwx;;u!Z{r$W0=<`Kwzoa=#Sad!)?`W9?R-!+qRJKS&U7l62RIU(*VJ^+ zbVf-^sv+8T6zWikH{a*aWr%6_c`;{#TqFVQ!x35ZE5E;P-9<{QpfN90nv~$OFb@R9X z@Eq%|uE1hObzxzWSuQJA^H*|4Y+}}xn^FE`lLijc?Sv5-(TPPIbCN0{T+k^ba(`&o??H`RoFU!SuU@$)3=!~X-&kXQ$Gu)_v)YBrqMW|LnDzN9 zPS?T3@$(&9AarA_WeIzL=e%uQ>K+E(auMnQb5wfKH}}L@v1JCbp81ML^9nun??3LM z&X2yaP}5NY<=fc(N&nlMtFiGSb*4U-$Gj2vGH-Dl4zQ#{p~Z6MO21qbR2(1(#ZA3? z&wZc1Ja#>@X$HkV(KSh9R2DjU)OGY=bIvt(=Kc}#FaQ~OI6A4oA)W`Vi{l2(8=K1J z5a(^bmz9xOymtqdrZ^|N6v+5|<+s7c>Lq-8uHLt}(d396 z%r)KC5Hlai`4oK8WXAUQ1t%q)<-py?pYh&Ks8#PY^BC@JkgaI|37+>@=E@jIk^Xrx z)Q?Jfk{;XD(^)@7?5*$|&vpUzNID`SCQJpt=+wSCpotXh=6H8H7U0~^6k7&VQy*JT zGssk)(sjI7h7R!~U)#dX(fC@TW&OI*bp+2e^bzCULThGa<>_II)y$f9u2te)L3!@T z^j9d0CMP|)^ZoOp0zBZ*y*}!_x+9>=(<4cBVfT!b0{WEQ@u^R^B6T_6o9i|D`nA4V z#HO1N5&&ce#=h6~Zc6W^&9$L-YR0fu;|$Zkuz(JScXSl0OC>yX}(2LMfo1UR~_N-Eg6`XMbdX zWr~ko0B2TSDZ2DVwXd;&^%}v;1Iu2lM$k{vQvnmZrxM5V{AnzYb-SC|&kN*)i`D>CrdV6q%*;IfUZKnPBg zbdZiLvDEYN@#UuzKu)p9SiUhSLbxC8#eaqEH6zQ`Kz=_WOHH2#$m0q)=H#A>X@ph5 z^0Nx7sd;LBz(}$zVLk0+?6S+cUSE!ntqj^QFvC;A_#`LB4*#DUjEnn2(-3@1gQ$Nq;^dj#u&1NHCdO`ukt|*G8Anh34%xP~<70xA9=<)RI$eS8 z6l3O-mh75=YyOdB%85RUwEHGcR+8IpT;BYNd7%P?K31{%oSnr}x0xw6W@(u*93b!x zu3j%KCv=9ZMEEgmJiprhN?#wJKOL~UH03-%AttkJf3wfO5=W5_iaLL~>L(yd# zN+c+)hvvNH-R_VoD-t{lT-FvV#d_QinfKJY%ZEPXZoPGhMnJdn{I#`_?Ni;VMH6OM z`K;8iR(8F>nT6!iUTkwPE@!4%nVxGr+mDg7BB*txX)i(Kcu;~ z)Yl4p_vWpywMP7C+F$XW;b~0QxtF<-r}cB5ij+on2Ab3SyZj@*EC?>OF7AabX-Pf; zz~{J72cho4<-EQJs5skhid-_k;*ZxQdc}k&grxxg`@^DqifK~1A`9L0(<0$8o?~AX z3ZrrE-(vkQbb0*aoiw&^77c#2=jFE3FQHmX*?VLb)&`}g(*HYLR7i5Swh=lSt;W&F zHMwS@O7=N9!cevhqD->?_JJ9mqO=gK$5toVPE6LRY09?e;Xk;q8(Nh(`YCUDaxutz`9TcNAlnQn-R zZd#WU$#wCZ zcPih*BnE9dVE)xCG!UKnx%_ciKBvdl0y2mbH8XTi6!gTVRU5$)$`2G!qpyr1)(KJ# zbO=_Z7dxhnoKAQO&>6H5@5@tI0lDj2=qQhl!g>*_YG#QuAi3ssYH&aG-lT6@92EqIIbRT4|Im^I=lpn?gaL+-4`YEj#?GIvTB`OYaCCeGLp<$}V? zjt4;MeR+B=%$Gq_R)|FQPH;UUf82c|7q|Tj&5fO9F`O6@ZQoPqawctz5`>8Uj=Vt7 zlP2pd-EEmLcJ#6)^<|HS9a#!%66l-*BZ1a9l?=HB6Sf9xrA_0mLnArtu{EMsT)K~z z1*`%TGmawWD~Y24>VdeN#PFDE>Ir7as2>-C+3>`W@6`BYh0IqP9JI<1~AS!iW&u zwBger0uy{FE~HTejM#P1c?)+*oB`(zCe-wIwp0i63)J<41oU@AHJv0TSS`tveEE6+3i;ZFHTCVV z*ES`Bbm)^x^~?&oObboG?m1S8!6>JN&T4DsB!-AI<}9QcB_9F6=N1;TEetvF`BDPU zMrIZGWQ|Fu6ypgjZ2f*E-3d&IP0J-+fZ}trsGHhu1gH^gkf*ysN5KeN-L7WYdqZwX zvR!Ag)0BQ{$)SSM=;C4*gfkN4>5`saI|joY)o)Kg{a=DYLO~?5jaze&=-9djHy|Gn zP{uyu+~aHf2e=06DUGe}q}J3Ucd$vebGo+?XsB*HTY=In`BvJ~7q$JgEEVhZMn#G5 zHs>I5^2q|Qt)^p1;iNPc^>iGRY=+W_bzl(!p#Hk8+0$7_Z4;=U#R}Ox3QnFbD*awO zU#zU}ptcqP@+{+bS&-<~vq41jyJ*!lz*=;sY&z13w1V?c+hJM{2l4Ozy-H~0y-;nb}-!SXBDA5iIz zZ2^4)EImL8v0Og(<5b>>4p&no$^(R&nv!DQoTn4g%8<0dP}#5|Q1G=8uR-0_+VS@A zwY7p#MeDNN)f>%2eQ9iD;HXm)aBa|bjLI+E+IkP@O6njbUeYGF)E$k1==Ygtr{7@f}2|=6PWW) zsNwG`gHsCasi1!@>{JemQ@La`8ckCk@X6s3G{SnSKnp=iSNFWXZo6eh0WBWEo(=sI zaRp7~f7ygvmYn=x^I?9SwLA(lDFA$F%ly&%05*DVX~|wH+ThzKPU2bDfFgEy7XC@> zc!q%%ak1G=h*~>ZZmD}7AE{|R|L6@-%BtfA&4B85P^x0hXi?j;LPtc}OM`xS)>q^# zJl6o$D|ae%%kIl&LFoy`mbB;Oo2dfP6W>0oQI~0na_`J+7{TYXvDXuOl6_5vz(#!& z{RU@MqW^F|Y*E~gHD0uujW@e*z~!DrgKwQreBsVp^+&$PuUUxQ@k7ZEF)#PBJE*1$ zQuWwoHa9KD-tvNE8}S2kNkf>8(E#$`)R=@iegxRFjvqA_(5F}_fhU4od9XA_a{-kF zlzHK69yoS%r}EkNACI?Hr#HIUV1J_1ng2wmC*Ey!d{9X*(X%ZmCUxhnoS3mxIN>7E zq?{97%?=azj}i)xOH9GWhArI5!0=*&_RTycL!c)Uw&dD-V@U=TGNndeYuETjl$iAS z+xuoOV-nvv%|V#XLOM{;Gm78v1{Vg>5&)rYfsM^e!a`vch+?-PBCfh~Vg~1p()Py} zdvwS*^x!Ri+`JtXEgELU4VR**r+8;CMBTCzLWP1Qhtd-r z%6Q4NbVM|oRaW04P?Q;PV@-PL02n1C=jCcdOm*jv^+}CM8}(6qO*2F(o+g7Xr16X0 zsPjm~blkg30xGQV|C{#x+YUh^@KKjUiSNQZ{NF5@YxMHj0tbu8)^`NIzZKW^L*fp@ zwNHEV`BXZwWZ9vp$#DDf^Klv(cm}c66rY-}NHZLZSud~Krc&;c=|Aiz6c}7@?aN!r zf=ePDvcc<3MvwUN@!Yv6>36I)g#dF?!A`T`XtYTa;%owLP7hFY(T5+P1bi|&^#?2G zcWdMLchBknXr?}36uU_Ka&sc=&qm0Y&_m?q(;RBU zdk-?<6+8@)+%`kpxIRhiy_Q-;N5?BTh*LEUW7<60F4Edica@3fe2?yT`ug=7ui8KJ zYUii@W6YxO&|eHa5ajWf>rWQ}5<8xd5T`5h0v!s)Mah|HLcJbfnd=;X5QzZ= z?O^J+&~C09kRZBZ=;Nqvs)Vb)CzJ^GtuoDe!t+)5&fsD9LHl#Z(LQcw z9)k2OY^x>F@%F)Kzg#c7!pDVz7(W({-&66kw)4`Y9o@_1>60s}u1gxa?@<~XtIBo{ zM}zn&f!U>=w#MUXjQEZ+V|)#L=UD2yg9d+7(Kw@;%de<@}ly|0|K zpUcl$=8FhiV>2~L2uWsQ4%4e#E)%D9$yWzh8gDms96|{w2U&TsmLmPXV_rb``VC>2 zX#L#nkWVM!f=-?=-u8&cX^!tBw_b4sn2bn0MXaS+*J5=@ByT0E-L}k)w=9^CMW}0E zY?y1TapnEBz9?Q0rH@tE0~hFcx9HajDy6t>KebEC5h8e{GmwtHYvf~gdZGJKew=~E zYxRWfEN+tEYKRW^&Pub6+nKqdk$VmF-Km`M8qo;yh-Wlt{)+`1FLeNMCxUc7`9)vf z`b}~%Q=S%@;`32o6t5lvmM0^z0)XaG=zQ2U7rk;&r(Fmv) zS4&)){*Z#Frz>~Noc6L>42F>nMVn{;I(^{i=jffdktPPMidINZvF7d!F&%F|brT+d z%Y^v2$+Z3UyI}Ug_})VE97bZd(85U%WBinVpg10$2Mv4f#!X)k=K_3wL1MAN+xYVTt z-u@IN^-hukU4;#hEYuQ15K7{`ioodEL** zeKg~qq|*qgyt!^7%XpsNi{|4oQ+}&cDLEPC9M6%FYL9vFrG39}12T;&j;x0`R6(nB zt24OD=1O?;plrs;vRJ}$PB6ICZAl>I@alt;#sc^86<&h6!fP@;wuaaX)7D#iY@w?i z`UX2gyp#%8>~QY`=?2&L@_q9<3Xz_C>GKUa}O@xYti4BO1P)gdWtkRn=?4eN90wmplhe9 zE7%Uqy)K()%V;p>{KEcVjOkav+qc8V$I&k&B%DVnu7Y^}LmCk&k(Z)@kS9m_*?vLX zWT^m1k2?_jMNa{XiDcYjA6;0Q?z!}XK3JJ<8;2%-(Q*{lIA>lxf>UbLeHNgakG^) zpj!(f6OfjoVk}4Da+dw9X$;@gj(%$xtV~<9ViqGUEheRQb?hg}wB>7xkw`$i?q zV{!ku+i?SYJo$W&sWxSb+$vD;i`hAr>J!dtTPtnIYun9lgHf%>rz35o!a1KvPe-F7 zEF`opoZdvzHXQXG#PyabT`yiaJE0(Vi_t7@!u~4Wh92(h;%=8bgNR*)sV_5<{;XGF z88?m$4IXO1OFy&vwEZPKryJ*S zZk{vY5+@PcUx8;P6mN^Z2**pf9w1j@CF*iv;M=zEELph&RIXbm4l zoIzLK?l4i+@Uh%po5)#AeB;%Il_d>bXcl((7?}fxB9d?6_r|l4Z18$8*3O5(zF~FN zyqm__(b}@xaI^2*(DP0W_yBIQS5JEXZQT1QbdTE;&A>rgbn6#?`yOOs=+}t^Ew42B z)n>Vzal2f^)FXEcO)*VBweg{up(S)Sopo@-%Z)@pNH609YXSww{QOtr1>ORd-5hd{ zyB}FA14MMC+|Sn3ty7LPtUX#M1u;GrnrNeqEI!(tA|cL_0O%CpPj1ES<7bOzKQ(q8 zI7**VGa^NVW7&`7nRo;uVS9q2&F*;I&O%q!*^NINgS1=N z1L^HDBX*_&M)7kL0SSN)L&UtQkHOX-%y1%+0~b02!c5P2x}-;9!rd(n+h9&U1@ByT z<31!1IUAcPorSq>PUg~??8i`l%ykt+K4e>Rar2c9mOD#DJ`Ct zy{p|Lz@ZlkfhBmz!>!Sm;pRrJVTViPU_detX8S8zjEAG|^L|w$#C1fw zw)GSSR<*~Z#dC;2!Jg8pL-qygVuSkXu$9m$^NCuDgL-J1_Q^Aa3%*9kTJOlX$@#(Y zjZUPB#fQ5%f)~`5GoEZu_qXHHcD5`ov0YL;Rd9`Wpfcd`I6?v2NsDfI`|`4f+;NI~ z)1fVpUGC@L%cpSz{2fD0DFE8(*@Q5Eleuf4XX?lj*_-Yi(-9w8;v^Gc-ZJMkA>GrR z*-oU9kS1VFx)4|SH%93H1k)*AbwkFYycD3&Q8$Mt(nfL@*xL}^xbYLVHn!FnnRmLO z$dn3r@zV_{Z8Y2@_TKvmNHQgh=||k_k-lq79SUJf%8f}I-0Fc$ev5BH z$4VXlx=>}kOujiIJkU$WjG6=y%9dd1*@JL(^bX4b+j3u*rEuG~W5;wl8)?MI(WZ%djqha|-7f=j)r{6D`YWfC zE5d|-)UKy>qz>qFV9b9#4|tkNt_}h{;(|1ed7tZZTrWKa`%BwP&ZF_7Dq<9BE=9w z#^vZyg72A!xXAt}c}Cc44#$vl9Ld30k@dZAEZl#s>@T#KeLJpDM zWh$N2D|^MFpDWnr^H<8FuvJG$@`%pyE})eYmx%dhrrMmIyYs53B%R|EfbVKC z{T1vV{14Q^Nm3q02>f)=w6|wk&>%hvC=d+w^`9E>-f`(oep113bq6TUgx5Gwm&DkI zS=IVXlb~hMvYmrdswOCRkH|KU?xo8?1{KKMX^QqayoxUG;Fi($rp}+00-hjuwmSM% zQfC9*D}7(F0B@ysUxL(396?XV>1P(%DJh-Y(vy%lTu9DRcpr6OF6D8u(DWAqZ1*q6 z0+>@84KcnfPGJKV;v})BjMZ@Wbl6M0-Jg&0`?eU=C3Ei=2YYjs+|zqi&qKFwP54p5 zOY%TS-et5*fMJXzw=szaOY-PA%BHD6wzl`-rQ7aZ9J<{g!a(J9CM!a`$UfTCG)Ggq z-xVzFhOmj7&kmd+;QT>C^tAr^^THe-Mxios27f;>-~^Ig;W*?4uck@OaZQmZ7vfui5SWI& zr-)GksX>YqV<3;8?h1L8zuGmzc)cmok_Q}Gm+v{6XG$`o9s>`T&Pmq-8oh2Z1TXgr zf=5!MRrX%%>r!ZWHxj63Q-B^35vtH`qPUkEe@k1+-*Wg-k zFIxVN}m7JyXi=m5D6}w@@Y|di3uA9p&S@^#=M?OtB1t zovPY$>NN00cWCwED!U|I(`533ih3`H(PwPe8$0>QqBJ4(^zNapAB=wDcZs#?3cp&C zZD*ndVvNQa4EAfM)$Yb6CLG+-UQ3a+l=~ChCoVAY$PN1;3T`69 zujJUUrPJ8$+IUZccCYNP=7g-U=eU}JQJHT#mRmtPt){`EJR20vrdaz-d{JaFf;Mhq?X6@}D>bk#a;l zoev^Hs`Dy-_b00u0Lik!>RX_XZDV5{kb))xZzRa0&0nD^6NuiCl@QaYP`64n*ZwP{ zhbmblp4E;jqO!3!{31SvU>SlXuT`Td{AydKojoDt0WanXHMZ@K4}1QvX5td}JX{Va zH^==99K#H!6i9T94Nt>9ib9r=Y!uWRp;{EgA?3cOietB-uX8H&)GVZ?G&bkHJe9OJ zk-R@ks$_SZ=4Hh3PJog$xM<+*9Rwyh89bQN-|0oRMdaN)yA@Qb4&(1s9b7Inhk%33 zc{JR%ogg+PoP`W}mBT4S0YJEw(_$(0sbL~1WXprvnM``0naG6=HzxC`HcZd?wxzDm z-{hsc`ZwC7rpbR45Xj<3!$D>9Vc3r(s9_IAZ$8*6P23V->Y*btZ3^3%7$t1Sqpp&Y zpdWu05&qBv9ZSE40Hfy2D$5f{pzr2I7b#J9NKRiZ^UEe-LW}=-m{=uFeq~LyC>Tgt zL%+vA)mx9@{AMN0)P$sxnx-P|;-$y^wKVgZdytJV&kO8H)+ga_hOiu^1lq3q zmDI9KQPCK6=RqLtWK_TQ^U9s5#lomRJ$X(yw8JJUCgBiP(gk1EXA|8bjnEO7mIVHw z9{hJTDTI6OLRD7}3lrd~9Dm2`qouP(c-0~VkNFLM2d15sDXHjV_W!%uH!8_zzg!By z#Dy#*`W?>Y)J8LHFRZex9yzUG6`-$j+^oc@`OS+*)N$0Ml>@wj5J z&ObK7KR#|Qigt2>pX#OsS2ystXxo!d{l_x>ZtahByyr=x_mxSCwuDAeS;2o~$_Sio z7sz;Fe7OHJ@aMf2|AY5iE1?gekq+q0rZLqkPY`0(L21pUx|@`?1v^bBwi0X(0t7^V zVI|VRN5Ez`R1QbUx2dL$q)?9lK+vNlwO1quBkoN!_M>uu4Gmg2EW|gP`tooqbjVG7 z{d6W#Sh^5>5#HMeLPahpz`NWbx`MB6b?#IZ=tysGtA7u( z7NBNmzzwS$IAiG`T3%kzG`f*KSc>!t^V^9|^%0CXa2CP%n_3YZbe5b!1j8kFu=t_9@k}-kZ zW%~`1V|gmNsnf7ykOmMG%o16xS?&xdgyQx|;Grl0UenHn zE*K~Z#%>c!WP{3faM(0HDT?1Pm=Isxi(p3)P-9*-{TEx?fN?ea>a)=8`#fX;5qh3__d_NFKu?ZY0SG<*g-b4KlY# zK?~{ronR}0j5+Ym*AxHiZ9bXQcf%R1P+Sr=r71T6Y zPlC1P%QBd^e!6cc?dmkG&h`ocnza9rOc-p`^VqYDIpP7Pv$N4k5Hpk(νaJO$w| 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
}ElG>s>)X2jC>}UeeDA*QE6M-oFv#yWcc@vq3Aq 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%0APH_{>yKl%`g+B4IXz9kt?`D~)28$Ro z%la@PKNchH3K9$al5e4Xwmu8(WTo_DfiY7nc+#Qhf; zrh*qXHOBbpOXGkn84om)E}ZSv7+*PMFAET9Xug#b7PgDFGq0^hhAV#XZ>(I;p)uys zi2KYt?RUvrK(vOmHib=TeD--l-&*FEoq~SI%=D#`BuVyvNL==jJ%9FhjwFx1;A~!x zxEEZv%)7-!*@9*<_Mg!xNjovz$UMvpDg@E7arp4kqIrs2i#xx@H5N|p!JVzB-P z`n|NY!7LAfVBh>cva_h`N6$&@wYyx~=GYpTB}eyZOdf~9DYt6YB(JGuE*R{5wFDzh z0LKO&My==8bPYEK7sKl&pq(r2$7!~Z(h|!#hfX%ajs2-dP)0a~FlGQWGYq(G3x@{b%ki6?(zTv$)M;Y*#0m|suLAaz(_ zdz}th+Ak%yhW)rQo7bgI_U<#z%%nGw)$ir0@FMJOd@{l!!RO#50=@OF@TbzotdSLDHxb^SYk4)lIlK=|Et%O7(_rIoHTqZ{3JYpDiB9lgf~=H8Umgw z5KGObh`wx1TM50h>gzXhgrxjXy&sV|yaJ@=m$^<`J{lFry|dscmoug%%}tKH@eKj^id2QdEk$(C787wJo@gy=UTA%9Y8!6Dsj`UQ3<# z$o{wR`6?cclQ^E3{<&|!&<<&A)t$hzufXl^u@uQh2fB31IU=6)jLqQ z!MWC;pM(K$(C7dd%8wrYuz>)=VsCIw2z7sQB?wM`hHQu<2(J!sdpsh@&uduRu#5rL zYt#*hBms=$i25JN&PXDK(E>m9qhU`5MFO1qaU4IU#Gqoq1&Jvuktaapiy2FUGX$uM zP$|7p13wNf$;lmu-y?E{KJX_IULFU#f_Xs7HN=qrglvfD&O02a^{bz;ps96TZc@g; zxz25SY;QQA|5)eTTC+Xw69$PScmhFq6szPyP6{n29YkWFteA}QkWz0Es~UC*&P=40 z*s`Qc4vccvxF$RHi11=g>p0n-COc|IgnqDoM42$DFjqEHwq0)XgxH=P9;+m&NyK@N zJu`fY^Du-wdI3^3s%ms&Xk)~FZ%dzXrSo^HSz^ME?|RG(aZToo-3$*a80JxIP+jpt zy7xD199A0Oh;V88u-EI>QPxK{fa{(Pq~})d6fKy7a9M%4n;IKKXRznyXZ2@3cjyp7 zh6-nt;%Ik)5`ou&t$}7;$f8U}fkiSkl%AL-q4+)6J&wi<=j7)s=cTEssbQ%hKW#W- zwF9&pf1=k8SuI;-No!LQVx%R=tHjL4-o$H5qe|Nie5it6h)x$zil2-9Hn2OOI6yj6 zd~kC>V=4TNmN#paG_?Q!fx59xn3|Z8(456xh%O%<7pv4*t+0}c;$MaH%*u>n|8pF$W`lp zP+5$6ZRA7vO#9sX;B{4gh)Re@07WQ`REM9=g~y&p_<(PQ2jt{8;WiZKxM0D+x8>xu zzWSImV#rd3w;hQXNoL}+iDnXX4%~FzM8}ki*Nji3cA-|34=a$LVxJl*C@l~(lQd%+ zNlKYX5n!iDVH*kB(HU7B7Shzz?$P;PXHm0PKV7+{xuY|yDOg9YbFaOv{a$Oi@`pBC zDO;(V=4wrVt*&h#XCfz;jgbzQPV6ztVw7cyCCOCJR7t^dHN`^Z(Z!L~QPc7M&HByT zW5Q#DcU)^_@Ub^EJCy2MpPH@y6g(~d|ky8H7-cRvPxgzc*BJNJD#BHC(MY8<$) zjVO)iFfAPN(I`2TpPMNwEi+hNTFwL30wGV#PTrg>E}ybk4omN7@0g-ShP3xNMyrx! z@h|glXZG>Go>g5AUh(eD2z5IK8~#|q7g(WQ(LG&W(Gx_{~xrzhb}ESJ*lluQ$z9VAyCxN<`x%Mc8X zD&M0pvEaJUtI$rC6zu_FDDV?`U6JS&F9oHE#*dKU(BbDVgWvDIH`#c*%HF_NCUL8J zll3v18!3eJsH;%!4_*&1$MI1j$DhQDR5eyn^C06Vl)e3qwfd&%())t$QjjrJ$KE?< zNA$ewFv13>RBJk!G~sp1_{w-?W0vZNoI3r!{-Ab~7k&-Bi`Jpz)OOI27ve`ml<$DD ze%GDPTGy4{s}D1^ zX>Ta0s9%^e*d5Q168X`-iZn9X>OFeaU-NMpa)oj^1Fx3dv(>8E+iT3HhTkEl;ipA# zwJ);z4JEQBSLx}5>0s+L4|Wfw)qSyDtT>hns@MSL`IWw8?{e&0e)s@A!n*Q*Qv>Uff};cOoG-qpvHfmc*Q-{CVMN_xO7H zVFRYfNV!`nx1#dfo66!%@x8EFv+|vX_#2wrELOqy9@>vr4{SYbLFNhOmt!dP(%LX% z`B{gS8LJO_-d*>6xMl_x?Rc)|7x~$8yNjB(=1g7&f-b$6gJIu>zkQxtD&x>~Yt?^^ zIApp$1|B1GE!nXzUo|W&vE*(JGpy?D0%4choI{*;e=cje@$s7QovzBY&ADXxwZDqY zewstQB#GjubC2}UeG0tDJ1TyeDlsVaD?M~Pf4E`6*GWg&XFFAPj67XKr+xpx~0ro~g1VPuEj#yP&X{1RA^#Pps;4-k9}u8JN3 zZ2Sz;(3w8`9I~Ri+Ui;EgXO5gW~Fq$bJ%^6S{nx#*c?)aMzIP{m00mp6YiTEa4?3( zO{eD~Jn~mOCwB0%D--jmci?v+kz-GO`{!8%k?Ly@FU7A=3T>nn@)(%GAe%SIR8z+6 z-8(QkP#O*l8XN-*5|jc5{egiKfI#i*$}^LLkMR>{z-d+%D_Za#bjhaZ&edVQ&XUmg`KlJp4&Jm z1KwU*%Lxn&hw9e@E~7$u1~S-PEY&ofHQ&ken%LPg8hx-cHf40Twg1%)7{5C&C~0fz zYy@z(wE;Tux(ks1Q-c?j{*}!{4)~{vv$X)Z<~t>Tn4P03fRmA#k(pc&0RRB-JAN?Z zRS}o`mmKsZKyKmeY|qQY-*Qc z2Li|?XcHjPox;TxBsvCiNuKjie9{$)oEEc{()2vCcw;AciNxR^l;G%MIkDm1j1sBZ zDx=q8(@k?`XcR@(4U+-r21+jv5fjPq={wT>z3qdetuiU%YNH;*TKC4fcq(C=s`9d< zvf;lPTmS{=biF>B?hUXO+`yA8JESw_SWRcds7Y0TPNbfRA==TYAxQh8-h5sM3AaW%pto6DvELz{KHK zc3k5vM#-SxeSpg(vW=ra9BM>5xG<3OM46y}__T}o$wH_;8VMaM0dgqJ3V)alKzWK? z?TgLrIYAWXa$u@k1N|7`YrvNZCHDa#Rsf99J zr)EF`&_z<@2$APz=6AfCzAL}Hq_HUR?kvzEBU4&HnOEN)ZX&m8FB&TqfS6&ZSV4jE zHBCs|JSCUbNyu4gy4a|!%`jt?1YP49Xt4F3<^ksOV4^@9%O?rNL>y)mn%cLBf);R5 z4BdPWltZM@T_hQ>QK=u@zv88ORg7dmfv(VlNX*zw5snm9>a<+^96nPpd+*7|!()wh;BaGqJ={<^$|8huCigJh zS0gZxtXmY=ynqSC==?cT%-b;Bi@foOI=!C!Nvd>r!M6Iz!{7f!c{jOui_2@2gVC4& zXUP;Uk(}mR!lgo5zcOOo zE-j@oJ0cZ#J12FpIos^v=jXSO=yK`HQ;)<`p+OXARqaWkR8omIn;FLy*eNZY;)5Q1M&7@PjD^IzTdC&T$Kttkh)2NT4}xkg=@ zAa8?*EbYunrG}zsOZRKG{_f2XZi^5q@QJyR$8ig;RJ&ID2x6Uwgj=C6nzXQ}h`h#O zllo`1x!oCA%0*9B=nWxwINWEBf5!RGaD{!v0Gymj1@E#%;;G-A%bR3kVq;U~?L{!P z=cJ^h5Ph*h6&*&Gr~dFi-hR>S5`d3QZQcW4Zbu*0>Vd=r>=i-`I(6y6sILLOua8ni z9|q>LrD(lKqtUaSd^l~r|HBmvgzU!(D$b2ZQrb8BXcFh@^NAWL1+1onN|KT=*bLfN zc@JL|>5P6&CUVPD6;D2U;2>kOik0a$5v#~Ey?f2J^ zYE90Tyq;G@^RA}?ZWA}e@HIYl{w)TinCs^$shygQQeRx$R>} zJny*J4Tb%DBVB^~ziU+|HMyNiGPOQAS{xpML_Dwg#226@=*tiApn_ajN5Fr^RftNf z&O7>`Cz9BGuBHlfSyLB6>*@}P9yk3|o)bmP2dCU~7j8?b<4j)@O~@6+*m4FzAYk@A z^pnla;bL-~%?eRn^UcVBZufv$h!XdGg`~YH@B_tu#UF_S5J7l6Fjf@No^G3Xz6#*j z(N8xR2vJc5ySIk#AZF&5J2xg z-uWs=h$Bv=TI3dw0pSBHO}QWphAKJeEnheI`1lPtErz@0EI;qxa`Rs675$mL7$t#X zac3Zxc&aPUh9~m`ZlOF}_5-l-%X+Y^!P``qCVoKgT=u1mZVzZjHMRZb<|o& z)0tvhSN7i`qyQbjPc1Bhxz@IpM-utkddVp8I6QTAT)#Te0S=k>4!4OyDry{WCDn2= zkEIs4to>6s#n{-mr@6xO+R9SM{S3fi`%|q2UvoUR0*^cZ3GiaE-X-zm8?#;GG{mg^*tBa&lFA^R}<4R_rZc*)-LDWg2*Ak7iAmEmF2VW-@H6OE;P@i>`sIca17QUU#9ST zAhb_WCTZ7Lh^Q7SUs2HV^Gw3-?KJdP0DQ_*Yjo? zH`y+_rt+Jz>bZA9s}(8`|7>#k)&pil_+7nJo5m(9hqPbX)03A$vmyqEURSu;^%%uy zuWylpx2J(V-{W$3>c?loVw6qzIQ43?cLX41IP*;-TD#UNF=(8A(}~S*#ap}G7jp4^ z+WCpJ?a!K|%U$El^mNh7oss-|o1shruVRntg6VuY^B>=0)8#Uln5+NF5(Y#|O1kTY zCiX<7@3SX!Tt^CvP_VP_%{{KlZ6_>g@Ii_il>0>D1*nz0=hj;xxZ2liyD?Z`QHc0yuNC>v z)z(hDPU4@QQTefoh*vc)q6HtC!7z(s_~XkuR279NY>^3S;rOqo*#Z#fapun^hsvQq zGAyBf4CiuHfa87?9SsYk=dEQyvfOly@O0wl7xOZH{(MWp*33*&zsf>6UgwkCq1WCd z_2J})5^PL`N=IE6>`g3W9=;Uzr(=lKu9OnOXY%PbzJr2erlY#M_^ z9~ifT5OHPkN#&;@qWL$B`0dy716E&OSYiV*#6plUd5B?mAD7)$5o?8R<~IW{OiN@u z1`~=My>~NiMVr33!@NA7zPevbU_(X=-1G@=nw9214ZeLv%FD|O{=lO^?+gSsobPlP zygqQ1lPTgesWn*O$Y0(1djc72vKLf|TsEZ#kES}pos#|Akp&atzbE3J>> zJKxINPzLrOqIz)=4=Mo@0Y~wQ0t^=sI1Cu~K94s}jYZR?nw6~CoU%hw|d#Kh)5)48J-)-qOHidX4JLZ2`qoi+<~kdWfX3w)S-xmmP> zZMr>T@Q5lzU6eGa#Qb++V+x@x z^u{5fCRwv^UFkoJospNe&nAL(2#&l3f7pm%5;J?&wGoz^I;AaQ}dpyrBOn@Ftt=jpY|EyUgk4KnZxXC}~-L`=}VU*U@EAmXa<&f`g;3Kft&Z zvv%KnyYeVj1yGl6DaFU*uyXddTTiayD^Qyc`E3lwAVOViak}fDQJyzAihm!M=JwEZ@!2U-N|T%`Se zBv&m*)FXDxkY^@3+@j4(Zb-oM3hhg81kQjmWO$)`2KBZ&gU$d;Gis}!&Cjc>Vwp?M z>1ManmBGX zxdrJT`;8He8&YJPYCqqvMHrR;j=A+=Q0_t(u(W@TTGstFHNN*P57n24t7Ap%K`(Kz zO9$MRBqGYlgaqf%lC!SRo^t`Byp|}^u7*U&q~xhoj>ElbruJmc? zYn8QV&=ic*vw>ZaL3@oUxqNqVxh-G_UJQH3t&^0_dr5hs4nJ5!b7RHdY(I9bIeUv7*V8v z-Iy**P_8|ow!dcZtAm+*WTs=8^gUq-hs=uQwEHRF#}+lwN9y+xJm^DMhCDDlnU*YI zJ7$xc%vce#O0CrsndcznryfseDMoFE_o3hNE#!>fgud$!%mq~OdX+a|R1#~>TVKR;$ zB9`h7MlJkS-W2|`)|&7F0uJ0B$Eke;oEke{HW6W2V#lt}$stA!I#clZjG{gF{SdBd zhUdHegSSjMKs~-G(`0>lu8WT)5iAcK-@Zn03<6bn1j_VVgSlaXSA1^$$X&-;ZwHp& z;TrIVa;t~Nr8oz;i_3af3u2}@YDq`F{Wc~i`|e$=6_sRJWE!lS(xMzQOXGb(+TKs= z@*Em`>!q0jeN6sHjQtM}3A$9oygm9E+upnLYA4+!rP-<|#{gvq2Zy7HW}Ox@KKR6mjq&^y|Efb@1}n_g#^$EwdOOB$^NX4U7^s+ zIZIph_xwGj#rz4uzefytOg$8BKQxK&o(P$c=22PGg{SH3?L&f zLHc3N4$N9@aA0$=ucB{iGi&4^aG{b*ikkx(cJ68t5C}Np%@iq9Ov9EhpV6G#K((UL z)Ibj)@gtu-Ejcctr3$&>wAYy=(YG{6=)=RU#)?~dFS@%TveYf%FlsSSLYVAtl=o2# zNc99ZG*mf8hWm%G4~EoVe|o{gt)HZD_#TIrt(+^RItOemLSKEzitiD5N3I?$U@)iW zM$y_Qj_M@jd4=Z{HDB5*4o8X7R)lnemV>Mh$h&er<$1@88zX7h%_2HkmPyllNc!p? zyG9HFFPh(y-t%-rZ*Bky=#;GSAM5i z`j^N!P5icNlJ{4)ua9YH^!!57^_I?Fy?VNpK^q<=c{)ka0KfRsFRk*K!oJ*asZB12 zbc-f*NRI5jT_4U>-hhvD-ol}d<@6k1=#o!A?pCJPAH>k9m(WVND}EfseUo(tE{Z`Z zWYJyieA4_XLeRH@!T-3|i~FL0D&#EG$+tC_kxiosP97d;e(wEvtU-1GQI5m>wo3{P zKZZ=8u<1RlN@(Cs*DUS9olWL-zxyR&U!a!V?3VbMp9$fgl z_odp254VR>)^|Lz(ucF<@wq;KaDc*3!X%i0Xi?fX9UGw%q>Ea1t@lmy1NCfEM!t7B zz}Jpuz`(gL*4rKQBJ^%4X-pjHr%!Vx41M=&IpHAxFpmC8*7dH=N)gqs1juiQ4j=KY zKodnj>Zo?0#-No2z?Z5vw&ZbXv0Wu`fhLMpA}w=#2eK^1p-eOSsH2)aLIq5{`2;&5 zKLu*2?KP;Zdv}=m1VNW13hO&vzrVWCv73ZF^`o5xe3AA<$wOPG2<5*T@QkOr_y)^i< z&e^?ZP~5!>ZFo*5WDkr$|G9C9J0Z&J&ze(7u@DqYV!BTdKZI4%@(us!{Ej!|F#|{n z$|AEs5`6nN;5hs-fS#gZ5d4E6zD+)9qzlt=hHy@oU1`Y`2xyluv~QaVcoN z)W{>5Y#~hsYrXYLL@nQnjJhQ!@lWw}>VX0|OhVjB4+0N+6qCW{907PlsB8suCkufz zwpFc&GQ}jG`EC%%AlTi7(=RoafRM({~@yJ^(&^ zqR$r?%1ncBUXS6$p9 z!z<^|sy;CtkDUwFpCRh@4J{rzz^)tnV%)mSd2w@C#ue-IRpkD$Tdbknl?9G_7FEA~ zf``KsU+W|7lhGno?}MUbhaq8y!}r~6o<1*~QQ6^(5oL9sCnM2>^BK{uNp!vHY+u2> zPNoqOOKAm6njcJCl6@JRIgov+>ePUumQSJe@S#R#I#798EItP?%{I4;(XvgEl)oMR4IjIxjS^?JxA=aLI&5G9x`m5MZTz(z9?_~QJgTtgxesL0? zO$PEj#H#0uF1IyOK2*X7l`hsnJL@Kz*eUpvzU$yMhRC|a?1-Jl@dXEFt7in$?Dbs+ z+}YleN1&$X(qwucKsBOdG{M{@Qj!;@1oCSA_|MTP0T@G^x1=iacLU4B^sR+Xe5=8T zMVhM*nJ7Y!nz{NZ&`?7ku-&Rk#dA_fw8|I_g$llf_6i=2!Zh!l_6S}wvdH9KuVG_j z(@*&I;oLhFL|-wtmc2x<+T`*UKKKW*1PFnO-Gv>{# zod$QR|hyx3k2ORV_xbUrv}PH z?Fv(&$>9X?TiuLrT)NmIMJ_-IvP5K%dV~8lCOm`TIf6hYyFK_9FKA4XrmMoj&Vme;;XYw z;+!Xu9-l6cGVuRUiG^d}ACYNv^15mt50|QTuJJ50f0jul@@Z*p+g}Yexx8@tEzIQJ zw?5uXhOVzVoSPJjW*t3bgmKpPmM` zt6f+Qaui8;aqx86{tp?DE9G^Do!+UvPU1vpJ99ZcUR{riC6~m`Nq=mqr$|s(;>p|f zqS$sm$x(pMmrW!tX4q>}>c?+r_#T9MMS~AAVGc35(&G0s-E0LB1B3|DHV?XWl~FfL zw)!M~T>R@F+#`cuf^FBcsdOAUz-Pv0LbzTG>;40vBd|btkU;5$R?;V^a!snzDmK6S z`Ag0MAfo+k1F-K@EinIvPXd<64WVf3L%+2AR*QykSng1fVE^B(myJe&*5A40M2@L2 zQhYTela=0|BCw9u-^Q5SfCFzosGnQ=XX}6#!`_z+t(C**^`3(w?epJB#Dky_$?lKc zssY1x$Xi1dUARWHY_1bovXxp$6j{h3uazP|e10~$bNQOC+DiA$>5O!u%%z{A(R6Eh ztDa9KZEzpmF?H+&j>pYAA@<4Dy;jS`Dr4|~yGUv!>&Y>8Yb0~nX~Vd{z3I+Ee_IVk zN&vPj()gs#GVAE4nq>4(Qq!5!oe>g=gQd;Q%(u5hjrY|aZKE_^SX|GG6V+u-)9uK& zMXWqLO`aU(Qht7*SezJ&DdikWdn8TlbK#=YrRUYH^Wif8mXajz>e0Db;&uG4m5!kn zS!LW`9?*MRZ6RZsK@CHy zrHr+WEdSPh++*m{ibApjZ{Db}(}n-S!Iizmt9HRMc4<}$%>NKdLPe2Ev2#tF2zXg` zZQ-+vUfH8Qq zmBs+SKdR?fT&lkuPJB1fNY?X8kC4_{u>1$Jz+W0rJP-O;%tp5=J~pejNi_0ViHO?~n_*`~g?>lC%;Bb}YCYQi*N*bD}pDu^A1>#6Ed*q8DPT|VQ% zO&h%tL+o|HW!bQ>Fcl3@*soYlT6)a^17rYbtATuYB89o0|13WmfY%lkt%Xgx*s|D> z9~7kp#WG`WP8IWbYptjFPFuR6@(Dl)TN)OrL2}rn_@LqBRr2nOWR~v*zQALl(3IC{ zJCjfQGmsV;&zWcWwxip5VOv94Ru8|&q{*i?ZdAbk%k|Cuc0pk3T05!dsQ4RZUF{iuZk$Q#WE#=1-)1A6M4?!aLDudgw}~>~(XLbGzb| zHJHq#DB$T_7)#-oZPXn+l_M6i@7C@ket>veT(dy<+eir(RzT-&WnlJQQUt8o@_{EQ zs@=q7{*WWDK|I@kR?I*AY+0l5dRnw+d9BW^=f(SB-<;j^N|R3cE7bGB3E%#P$fv!} zTq}9khg?dH7u#_9>E>A;sdQ>Z!gek9IL9lkX(ei9@+>SYSD*m*RGsaj<_p8m&dw38 zWoJk`H$BFH(bxOaSBuSpche=Na-MfNu$H@{8CM6^uU|Lu`-*)YU7fp;=TyJ2nr^vz zEW%WO`{$Vdwb_}W60&R-GlkjAnX)goH;T+}6|jcVH@Qy?h4dhxLa%XL{YbWdMHVoc zofd8Db5f|t@S14j3)3n24dC3;8bNbnf<=6MywS+n!1~`C7+oP9190P(yDshW+8>>`8T;J_MUTk9Ea^SG0&Itm9QbP!A$^)J(GCX{-wEh~L!5sr= z{g@DeBErH6a5>*sQstOhp?Wbx!{u~^Bkb7(`r~Kc#$sd;*?jh-;^9}5Rybr1ruwhy zar`Kpi4Gv9#~?eOYc@TV5zU0D7tF)F>c`#)ws)knllpJvDlvdLIQN1jw+taa*TD== zSP{oxsREtf>S+B=s4=Tnm(zFXl~sQ=db%~#Xanq?uHyKQzjshTvl}g(>F%#G6*UpN z#oov^Bm~ySl!eyr^fB^%IzH3|;mtkB5&!g1Y&~Lkmm;u`CD&cJwq~+RXOQ)6vk&sg zCev7rb=C{q^XA8no^>W#cCJ)3{6N4tK0mrai{pxxb?RR&wblYUrGzG_sjg}BNkjt<(s*YTS?0;b?bPeBy)`Y4Fk4QZ5A3{HOm54eseH>`lj|h$6omKX#PVZf2<0n z81!-I3;IzqQ2Q39Zb*GyM0)h8H^604xgVc5o?A`Spibet^}pn+N@h!|Df1>Ji6$P* z7OMNeDp$^H@{TfvbD%E15wjxx=isERf!hOqB=zsADA+;BOZZ&2jAN$pIOH`#uhV!b@u`9EtVy8@8i z1ADqmKj>!M7}o8$6|_^tmldY8f(F3l*&=2_^^Yu_iZ-TGH&ujQWjs|pKpPBiH=r{& z-Mcu1*GMkqq*cnT!seIyEI~`xglC#phqU6*XNGsrBemNV{LdAp>@~8rXER3835ndh zn@f`#p>yVS2TnNiH-pVcrh^GzRVt~rDmnW+xOTm%M9hQ1j6_i40ARR=W2kkO5Km)S zfk80aB79#JOjtDZ7-2>AfC?lnZ}zg%1YdYECRopg4H2U+lQ!^)`xE}%1nG8g?pq;q z+BZ)&D^2k@Oa@YXZZs;|o=x4 zz|w%r!1pPKP+Dr}?J1)q&%GE{yQAec-DFQ0;408OL`;PwCWr2r+&l+*ZLIKYNeIThe$DT}pB!;%bFyjm|5 z_Cqm4vH6B_5_);D%3FECVcbO<+5wU8>i)2HSudiNQHXeq9~^;A#~&i8(c2ngM87%A zIN~x1W$xN$(*HoctYZt3k?X6me3UQ}YY%iE&F8lw_AwRa@YE4?8}izmM?b}!=4=b( z7V8t@2bTAhtmrklcR$if>@9G3=eLvyTGF%qZ1^fUk>1{{HVWz6}`QL?M2F__%% zV^$aieZ1DqtBWL+*NLS(v9}0s^?O2G>L@B(6b^ZOrf&kNm2RZ5arl%mwaghzt=&jv zff11V2V_hoWymF((IFCVY}ADq3Po%lQL3+G5C82DzH&zEWD&WtC?`sOfpWPS@|(=C zv8mOloOfO~%J4AQl%``j-8HA<=wxW1Xj-Az8_Ta)HEVEL73?e3;v_^n%!l}m7F4!d zFS5L|$4hc1N#6@TxE@QeRf2geq&cU=si*{57)DZ9?tisrijE(?Uw6WpTDnf_g@j^- zSnvsnR-%iB7C6)HLH z5+lVifr*#eF=gxtS!W;Yu=6hFKQ|5-AvB%V#5L=za^0PUeTCIZG>l-u%ax_#REt%D zxWkQ%14)nbjZG}>OWrx3DOK47g;9VZnlm%#w8epL0`ydZ38W@G9W69R&z7#@c@jm{ z?IxS-gnOgVLpWIIe$W=?JMdigW{o}{yG0S+Zge#` z8Z!=p{PK2v(^7GgY%I5%2H^uZygtA^MmWl>e38RMi>F-b5> zFj*bA_{Kz0zb#n*#R%fFRzv!U&nscwG4rudy1cpQLinTy6J(eNgI$rg4Au*)#435W<7z#UF=Z*{3cA1puex|Q%Ioj)rK%5 zS}Ah#?SwZq?k(z`gq!a87eUhth*M1RHPVBrL>M;gA!y@+6 z(i_{EKy}vj)Go?_)@3vsBGLuL1I7{Fg+_&)@ipG{#Qqpj|hxH_v6<&5iLYFnYr$ z0w~@DIuh8yzK?j@>mcyx{bsX3^mKP?F6Y3cRSi$tsIph~> zOu=jvch+HCydO%6wJm^b(VMB4FbOwF*3Xc|F5-_*?rv#)U#3PsQ>Lbm_f`Rr!qESo zceN_3tLM9VucN55u+n%i!&~<-9N^NAMYW|Y1B0}lzNmAKVyi~(BnAn+!#T4m=Y;wr zl^{JPGcP`_qM^((x}3WL%rnE~lx9j3L7EN|?0=P;M>LqaF%`%7i9jdQqXQtC7)ie_ zWeX|ifTLteNdKV_N{yyUY%Po?D}NE#$|CM7&U8Cqw@>8h&(B4`&k>pdF5~XA=}2+&;C+x9_!XUe;5h1LCm|%49#K4QBr@opFBXR8@vILlr9MrYti(f5ee3Me z{JYl-cDeq^U_Gz>!-a@_QIzjd=H#_$*YC|XIP9ieqm~!Cs+#w3qt@RHWM+jw(Fet~ zB>!x65Wp8IXu$nY@B|ZxAhK}Cqi%^08~D0jC5TbTo>qTe@xH(bv->|A(iEzNFvMO; z171J50;Q7>t2;B(x0P?AD)Dgtv2v=I7?z)_%pg9_BLvDCP2W7|n86PMzKf8g!feHH z>Uz3D#OGnkAhQrx-~G!;({I8a8PJ+b|-EPxEQSkEr7iH${fl2v7x-UV$46Z`n-mwQp{Y&_05* zV(7FqxQ86DobM@>*Tq0b2DM5<(NUZfnjRlu4Sj$o`BK2~ZGw8`0ph;cARi;qScMgM zj_|WTGykyJC0q8$^rKX<%xAhI{P1l=@{X-<+^sAx=2m?@-*h$~YN(CsZHnKU#$k5r zm*9LSRlb0mRag^Huu1IXn-1tpt2K+)MY7ztlwYwrOVW1H9`3PNFDMl)(Vi=oF`b7D z!lR}2gE2_34VZ@6DJm-2f_IAcYa(YrjFYFJa#?Q9q&ac7@~AmRVMf^?>jSe=#iu+dVNUm7u!>MrdXM7o`YjpMzb} zse^S=C7W7zU;iwr1ZS*BdalSz^h%+IZ6Y^Kvh}DpOD@o^Z^ZpYMn{z_5f#+0H;bm? zbw=>vdI$rUJ9DARQGYIDLh%X93XjjSvIGdsvl5PU&ez_DmU1zf?WdrzHjGbd!RQ#z zGm1ry%jHr}Zhm@nXurVVN*GqY0NBlMbm62PE_B{iAiMB1A!lAoVSg^5SX7_tW%3qw zN##V;vq&lb<9~s!$~H0D?dpXXO0N|&2=sR^oz#DQje)Y3Z`N42KF-Hc9q0O#3!;O%3~G=g(G& zMQ*p87l0o2m5I>xWpqk1>Ck}fCM=W$;Nn;CS{ro(B6J#BCpzcWE+E>4wyXGs>CG@2 zz|8>$Z=&~;-N!Kk#~y26oQuvD@`Q?E*bJGYb2u1Sr;MTZEyhV4 z=@I4i-FTh-^$Ra3Y(p#+UBVuxV5=c;6uXf^#b1H0=&taN4}+4ggz1naqnr*ZdHsr} zaLrg*pn`_RMD%3~{#?kN!uw~L#Frxlt@(lGw7~zl-oj*(VAk<5fEC-Kp|SOE`n9aY zTT*>Q+kDBf6;Wg-u;(Ioomr|Es%T^aY*290%w3ufgf7P-VQD}^I}~Z~DM+pFPH>5i z_!_z%A+tf7INF7*z9<`Y&9-Iy7xb`B@5QB&5v0f0hnjnpng=BaR1+g6vlTREOLx7| z+qk0(C{;6nOe(#pwfu?+eRXP+QGYCTaVUNatQbw=Krgj2P0k2Vgu&T6x7(5W;>?s<(oK)2dH&)>cpGFuAmjv+^UpO|h@&R-ul$84AKfq#uE`|LMc%^=Fo$q4*2M|5V z0O8>}Mv%@wzaF=#m8#lMsZck7Q#Pi>%5}tSKA9PBgg%T$Bcai{m_G>_0i`b4HV34osi>Kcmq*7o&M5~;8Myl&ZRQIkA$DFe2W zVlp&K?M(@mF#vrUT#0cV%LGfMh~sWtqV`ZK%jnkm9ZQHhOTi^7(-+k|I{yw9vv-eu-d5$Hg68FhF^GD0s zO1*3hS2TY8hEL?&1;HwVYt4hi?$fcX&8a$`y9#Gx**nScg8O-%`20S93E$kIxQBkqM7NI!PW*x; zzIZ)0RWC5$T2+0#8CqY-9eEp_J$3eYg3YwNiYDB#0&tFeuI)pB#s@D88x+ zcx|fLzOm6IT`!SdU9VXhh zM}wKsF4q5roVx`GC48@T?i}s8y|a)B2@!U$p_k3weB6}YpsNUxlo}}Jqa^hN>#>wt`s+pJ{-$+>1PW)O5?18_FSiH4@R1S zN$Tlq*F2o_Tav+6!#D4LG+<)79S?aw1kH*`fF_I3?3GFtyuT7GV@lbK(cnwy+kd{9 z%*W(K|7m&i(WCz3u%bSA>r|T9aB${X?3LQie4kPIK-s}%){hiwva`KUgob!nw&{Vd zl!Y%UTW{8nR?meWEnyn7vxTSY$#cfSF}81_^6Anm(2a3&nv9`w3(an;q>* z|1Gm5Oyopnk~9R_RUTsVC~65<4a+W>J1=#fI-|o$WvkH1Z`0aq@Mx(Mjj0GspTwPH z0Os;;I3_r8f7cElg6BpoNj#*kCQiCO9i#QYTa&l$Wh$Z%3{%IDP zuE(gSI58}bHl(1b>FQ6HGL1O#?I>$9QQR*HD$19k(|Y(-w=s`*)+1HVrh=9R^Q-3p zF1&?zy=_rGh*?`j{Rk768y@Mai=$cTP1c5t7m*@GUinAf;v&jk*~xg~y@S>Sb*4q! z!>yB8{6ym8-rGt|Lu-plUUZ&GCv>?wr+SFqg5_x0d`BwV^B}$}->+=5%dqUYg95HK z{4A@cRFAwSI?Y>~;iUVM^~oO77@v!eXAkqt_DP;LgbflN4SfNXEBVe?CcJSEy&ccy z>J06}Z040ZJZmZMyV1!c*ZsPUO@jOF6BWL>3$+}ik07kM`Tc`>54n?`V^JU7eBPNu zg97(@JWH^ZcgcZ!)q_5Cy_8T#K0MA(&v%m}F}#E9{TJAU*TIPjTse6TV0|}3CGPrp zUX2ajO%o4s{j&-0v&?yF^A(p)ySr|aa|wYn#g+?d>(pQ+jZ$WGo_PK0rrse&1>YPy z%NsqMs zadO4SXswPk{eqBge%bLpQ{fyt%|KipAXhFeD~oV4%!9QezORBVK4-z}-sFs0lvyn1 zkX0D;3TdK9<@E&1Yco;;&j1|fQsa1C=Nqzo<~WQ4WywJWrsW#VX-Y~;fp_Q2wJ8S- z8ZyV8DeL)E{Ih-#FcvJko>%(ubw;$L>W6TSLw;Vlq(v~mvEhEnb5-v3Yt|U=aHA$J zHS9EsmHRisN;qc=3kzI%7h3ZGcK+(YIA!2t(Rc~Yq9*ZfP)8GD96E!pU_=USN?R61 zlKfJ0#82C0>KiNbAvKf(q@!z!%F8gf=lwMYblwS$qHGphB2aEsv2UK9chGX7q7al{ z48=3=<(EA=-A*Pt-&iAFEpQa)pU*p#J9YX-XH$`I?z(u z^&Hb>fkyaFyxwMl^Ui1ru3D~?8jpyV8^iZ5))9-c9>u^gRE%LZP}W@MyoxA@0lKR> zNK`z+Iu8$1-K}3SDavpojcW-3h{vz z5f=j3$A#K|#<3*u0NvB8W4<_p!+Jhv&p#Kc6>cu*&e3?%&l74%8qbNC7s zotLUr<|UObNa8F8Gl~RRW~VQ9#M)oS^F3XWH8P}4ZO$wH@#Lh|q*inP1E@f0K(n}+ zOHJ(Ihm5VuF11~gSGFKYfFW(CZ)a8OtJvFbL+N;?pon9`bmDmMI$>YBRjZ!bX}F6|C07J4ZM4(> za6)e^!ppv8r^ZhvxnZ`5pin7nXWEG~g($!&(#KCOjWu(jY*k>sMC}g^J%LEb@-1s} zy8>C$WHelT+Nn10MZvoB^$5(RbUmLNTF8^uV~pZl#-S&WHO?dv3bBuqQMbY2Y6Sz z#SPSycY$}VRPMC6-=9oO2ayZ{91f!-}sJ-w_3GpsAMuKe{@(}C_zr!r_1 z@)+7kRg1-@QNOH|`K3&eejGRvGKEpARtR7q9SMW#8}g6&KZQn4BN3MZNxfX4N(;OsfM1M~ms5eg8@4#cv0WfaG?o+CAJ}VIXhz1Nm^c z73NElj#D^q3-o%S?~hsy@*ra#JkMQER}?4stIQYbnCJ{*&?KZ&fV9@Ji4Sk2+C3 zju(92!}c{q&X!a*8wPr9ZL9{u`taX~W3`g#@n>?^8&Uo7FdUp@F`Jz+DK#$Z@N!pd zf8ug@XV7Ykth}Y<8}u@{*rkx!wlsq|q`BIh`EuQ%Ql!Awmy_dKilSIc=De~EJ@O(vv1$3 zD8WvVcJN`(#T2$UTRLWo&J@~YYQ6Es4=q|EbX*wi|BbNrY=7%B`g@BM0^n^?xHXNU zA~L}6;{S!)f6W$D-Fmfl{fKDTo>uw#)#}qEswDs65Aob>7|81J;JMPH z=Zmz)Ez6E{C&0M$#Y6%XVZ4l3p~Qs=WXYc$RNw^v>S2 zH4YvZF*b9gevA(LSKjJ&7_u=O47%}!q{{To;57mmvIuDhu9j3xYU~&B*spU_1{%r| z^=_5T#?W3F&gPTEm<{|wsM0PMTK5M^fz!Vv_#-n3HBJ~dC6!I94(WBB29xON+Qp2|c^Kqe*IanX&enu2Qv+?5TcRe9nOhu(qT1Z%ljs!nTVpZi ziD=auG_@9ES`_0U$GhEQ>XAPZgO`vWA05UG-rLOprStw?rr9c3f#&IhyVD7AbmXXr)Dk>Ar|?k$ zW=fJ;?jE6b@@}QX5AA;njXr&_TSQmpRNQ`0LFnN)7DRsORfLCacNHx9}(`dcDvm=1Gn$ zB4d~cDDR-|yf3;Mu=GdMsU}86Mj;I{TJB-o1fXkF&&l6@o%QO~Eo~gl=astgUq#e; zq%`c9n2&9wI_!_YQb}&RBg49-lw32oy`e4!GR$Z3at)`@^JrXw`-QUU+e(5$*CyA> znpP;RCi3X$W^UjuF26`RNv~<}w4t6vS4us^H%!A480H=Jcg$BG@PrPkmh>;Q8^~a< z)5n;2?PX}Mf-2$Q>ClfZGFQVN;Q(N#j6Bozh-#UFAfsNbOCIgXQ-3Ms892Gr@R=l5Hy}l-o33$& z2cJjqibK~)kR2e?7|ssk=oj*Z2}{EAImOA=CPe(pAp`)X@0S3-A$!AZ57H}*YMDU0 zhsz&^PS7Wms4V~d0o5IYN)`^^f%@3*Z2g>}Khe%`guTKQMCm=uZle+dx@&1>@`mEA zG$y)HxeaXr!Q(6!8wz62U97c+*n-8``UVE3)E+|p2z$}szHR@JG}YFs6-U&lJAhJb z98bh;uw9fo_=2j?;h7N&(4Q(6=5B^|zs^{w*rd!N(&?0FO0Lo%*~S%e6(@IdTq*@4 zz8P9qpZ!Yyn^5vfsq6Mj&wPij<%N1iqptAiB1@=IiYIg z=Aj-!!u2R45wr^j1EDPjbL!wdSJh^a5)y+NOW0v&CiB_4W2JTEW~VJuob_r8 zSrV15l3^V-TM^9$bKPhj>(YbiIQDSNcjVHhrWuu5Q|D^s4u~iB@B;$%E47|vRT(Rg zTV-<9X#S~{-0x5)ex=@miUZ+fQ92ZBCq&?5`(1?J^K$D2YpIgL>tml$>d;cO-Rq_C ziwEt04g(SY4g;RgJMczewglb%2$fw3i@M#Nbx2o$cOpQO;r=3t_qxG!g=%f^o77;X zxMx_mr5}Tm6&>rYD7K9yZIy)N8Ua|G#M8YtYe~_bvtSQ;^Z@Gl$~g)Y?N3-zF|Gme zw_k(AA=O-UmL!1utTsa5t|ELZEbEQ2PJPa9Q%KBS8(IZe+Ixzq+U}k>d|db%Lc*nA z%Yk~m8OVjTKA(b`fjKbn%;=FjZLmhCklOaVt*+?oozeCgVFt9PP08)ZS1WCg+bKe6 zRhXj;t^-_TWKq1>t@)#fUoHoQApHm==w?jG5w9+`F<9H^m!!TZ_Hl$(D&h+6 z2oP5nd;t%#?!f0!kA%C4ux{73?FjZxyQ_tC6>fBI5eVN33)>F*_>#b>10z9*PX+`N zn*kbV?`>ZC&}loKYc=K|@#BoREquqKbtpkwO$sFg!|KJg&#Hn7sY_{0^`$`9n}-MX zi(vF=wZZPbdEK8&Y>ZgZ0{&*UJ_H-i`dT;Fn3(*L+iE89>pF3U&WHnz7Wqv?0*XZn zo}ZvSV;*J}hE)$6I8di|u?l9FxI=8?ZE`U3Q?46B+eqSf%1VKYe^!%jrS`W zqCE3n&Te!jR7QV1%58D@ipq)1m72!pV4c#YhuE$AZqD9E@KAQ1D&6PSzYu6LIo(*laHGu}*j0Pycc!TW{k3!U+0347HOTxZPO+Om`EYnpJf0ov+9~Rlj#Ry|=s8 zEzZIRLA^$)5zAd2Vn$(`-IW<#j?o8Iw=Me380MP&(&sJ73PayOz!TaVVRsZ&&s@UJ z(TK)y4~c)~h=5`kgG&N-$DZ`sKVe84|7xxa?TTfy+rL$#N;Rlo4kNnZ49~al7vH;eB;;Oc?LrXc*sv$~m* zIn&Xk_BVK7sX4msb9XzzR=*5dX`D5Mm7^d=gHOlHfro_{Pe5Z1%+f7$=77EDWa4or zBO+cO1h~hQwA;cNl+Zv9M&}5dfgogenQC51U$HMMmEWwWJCw(8$NL3LScLf+C18Ta z4*Pvqq-TN9kYnYh($Y#*aPzDg-u$7Btj5=nXr*U^-YsV1^GI5=ZW~YVGZi-vVkU8@ z4^tb9!0RE(&2kG2qS{W`)6@FpD#14BNr$V7Jv-qKuGwvX8=ltf{WiIkYSNx~QSYc~ zdgBtv4noVM9UB+)&DWvFImBmaqwDGF%z*pSqr+vxNE;<42x^r#J1yk>D1yXLSFLhg zq^ZH)+G$(#F`_b98W-6xKaJh8XCoY&6}Vzm4czcI5$Z62cp>oC6DHP1hhp;^UcXOXEo37)u5^G~4HEx*TJn{eN;$Tw zG0xX~{Kc=Zcc{@sFQ!Kld=6t6*#X}g;9M+vHVu%yi@`0l+ML*;@Yut`#xX$BAsp)Q zLjoUN|09&vmUjhjH>SC?TAI{uu07LS#y0Y^#)>R*r~Hy#qQ8;{3ZC>DO9a z{vp>P?++YkKIB!Kb(@u(I$`x{+lYZR}-#Lf6BqSNI(?GM8T2)`Wl=H9`mVzb}c5`4agZsdNk zI#@zMMx+hYP^q%(zEuv_UB1F3+HSCOBLZz@sXFQspMVoJPEGyM!kkUx6Y5t-xM2o=v9B#}F|keH_ZUU4s6`-BNU zVD$bzISdGW^M|zk(m?GCF%YZ{XI%o{S>t-XOyXLvUn+U-ODiFS%iwkrC!m9}b{h)x z^;%N%ZP&;tQ7a~9BK{3uHRxI0AM*T%Um~0U9+`VfI|*V?kiMGmWak?b{hg1EZfaE zfq6SwyDnzb3X!sat}jojTh`dLZ?U~RCkw@N50q+vY~QhFRy6Lu3?gXA+i5yft4x3M zE;(E2h|9!X4r(_HEaD9N4FKrfEXtCwz^odqNAA6auRxbjd|j}+uL4JeZd?O0dg2GXd4 zz2Pu+y=>ojnD83_*Nw<}jnVpFI7u@sbz(xVyJH~U@OtE8ada?#`(&=*6ZNtMTmD#N zYFM=M+Ft3|MEQswtr@hOFR8z24Ab`<6Rl*A1Z#I653uWHf!GP?oOQo;-q#VPu!HUx z+Vup(h^-;9LL2xZ2>z$X8OcL1O&{qXu+uV?0?lj-q7tW#d;i(iX(@CXS7$x3@w+yh zsoR*KMY7)^vdgwRnB2{JUlW-6+xv;j?=PnRct8b0lrC4|kBP?Xg{QG)c(?LsBU`4B z)~gj-SGHS>A<)Kx5Q42Glv-)~q>hf=i}?6@@pY6-E&nB>p)|w8$yscq^(HMvJ4kpm zXCaus#l2VBt)4S!R8Bnqr;Y%u$iTXl6f|)ZD6*`hn_t~C|N8nmy8V(fk>q$wO;ACv zgR>WFXWh%q=nvs>`941`1bGdUpA})}RZriVXBm?I_-MaMy#MaC=8(*{mLoEj#HqP4 z@27u7g-eRK^M%;+*#yG)l{*9RJcOH0ux5+*Yl;e@wGE#Rr;bl~?`_RbCOA-MlAb5y zRK_)tSjRXy4fa{UU^V{B7DHk9)r$FkLFo`!%MTnX7uQ{2NE_pe+tXdA&f9%*E-f25 z)?f}d%5FGRi5U@{yF?&_-ygP8!Yg6Ppwl``J&PNi(#Ze{i2t=BPkwqBcx-&|%Q(<| zRj7Pdg%+1J4|s&^uDmKKLaZ~J-i~+$a6CM_yj)46%9iYSe<*HS8u=-;1Ez4f za+zdOhm2oAUq@I02cW%FJ1#Q{2Yd`MXQ^@BYZVxk;@R-CSKe&zxmBTsMq1WJgW;7Z z-#1&YTr3o~b;^yJho~dEDs7{DUjlbs%|SDn4`6Ql^(5rKIBgw~h*T(S!V*Iqge1v@ z?>{2ga0LtRmP~j_p5%C{SZHGH`l_)3s>1t<4o*QLFb(9)l+eTeaBn<2m`~1F5d1K% zS)rFE@aYznqCRX0DHU$jt^~^07wVnX%>IxQ+h1?6b6364jTaE z6Nye{!}d|bA!)X5cE*vj$y-a^cs+?y=ExTat6B@YdV&Nyqa%p#@75)=dfB>~uT4G# zvEu%8AksO}M@tQeV#oJ(|4~EoOY`3uaT0k*8Qdw$z-ShZ7}>_2#%fF7Ktu;K{xfVc z2myW!!)>c&N>7862r+iCBc4;3V57OmlDW+>32k*Emg?^Us6}p=Kup=q_3$vm-(~Qf zOgIug%}NO!40mzUh2*P(S|Faqmb065*}qG_fqnQH6aH7o@NZHr{nKDBO*D-!{!fYH zKYC;g!g%rjkv)Fr`v>hkO*D^#F8&_^hVl*;6O8&U#TV?qVnonYAOfz#(T3&!On|)q zXt#GV{V)Ih${?hFWZPxNYUKZtZGJ*AAc{@M2M+Vge`_tag}Y!Cwz&^~8vReb1DJ5m z6p7seTx@WL>`sqdi($1oL%tZ2*HKAd4X`&|)&y`0-5bpR+jPOZ2|ubU6JKV4H413#XAYq>uK*_mt$ zNCb)9-kQYCMW&t=bgRS%g=V>P$}6h`y@SG5=%t01riPr+T(i4|v>7p9N0$^Z%g;+ph<->m25#er=R- z7BD}%omFqL*sqA%wu7F~leXs8dq3Cd!@p8zf#!A99Wv8to&M7aq`FZoRwC=)y}hCN z%>S+Q!(w=*U=E}JcnRZGAn>|lnlDj}@P1qsFOtjTx{L8?6Ty<91$BX#kJklfiB5Ao zavGD-z)Xssm#n?X$P~z2CkhJK{7JUvQgU*ti=`s_*+e@K{T{dBap~jKpQJtuc!(q; z!^;I#hs&OIJpaWhcD>yAXSRJ=JhcsdELE?>cRFWV1O@s$#df`MWeL2WnC?#(L-1@meex@B%rx zax`sZ?qf*2?mjy14=<65n(q%hPMiPV6{D+|B-ZT0msm8{{B*&(=Kda}tqTnOh9noZb+mSDztmb0z-LBYo3aC_>)rL@D(c0+J zRvK+V(BwTjxJK3>p3|`Y$);gpt}I9i?$(#?;i3{?I)VYQr`SW@xvWuepSi0qFhrHFP|pZ*mJU8vBN z2j$OlL47tA%YA7WGsU-IlP+!B`A+a_Iu}Z}H!B`^i!pOBt_=!HLK5kp0#A~qZegKN(3wff_Vbb?nO}@Xy&_*0 zM{O8ghs#5g^+7yliCMT!zR_mA$a0as&AKr&-0m%|52E@|JcBd_1FWmt3PAGG$wY1|)Xtm7#kJ8Jpa|FHFYq zQi+s;tqr#MAgSn7mmkDZ>zwuh`e5K~rH=<BTKWyLE1>!ZxPC;ScUZqxfcr;>jN z-2Uk7?3DOtb){1F`J07H<3s^NXS-383NjZj3dL9Psw6dfibdV*n4HW<6gr>&oa&eP zZ94XQxSGxO_F;j{X|%kt_VBNtnaCAO97Oqc7?@_$!AOL~)xP@7Tq?W;>);ci`7;P| zJL1eVu97EfD7Ab9u!aX@qU@@~8zq6uXt_4zu6y+2oc3lhh*wyQ49_Us@5P;gJEt$`*zJA zT39@mg&hmI_JPRkuWvr&_r;)IA(N-MP?s*s=*I(b69kD&s z4UK8e^k)RaE4A6{rbxOl0l6=2nW`-+RM+A9JZ>ra!a-ku1hy5_ylZp9;xL1JwitwV z;#$q1XNe$ksk7>@f<)80DhLkWh$Nt_)GB`%uW^t1LbUP2B8vAU>!Pk|rZhGpOCVS+ zlxvHdXduD|EF@CXwi-aYMCzBcUd z2e~Xp3^mN&U+#rn!@(f_3+{D2hx%?{MP2;{<(=$8WpVG%Ho^>3X$a*W>)?8mqXiD)raZl7M0kS^3+=JA1jQ1z=mdB}u**SoAcZ-xMCLO)<*}Si>qgs`N3H7CG z!_nJ!fu#Xi68CWfuoT&|EfQSi{|-I>VS*_`VKXAXWImu{3iwfg!IvM9p}k!#p8B6% z?lOcVD7QE5za_-*C$>)5nI+pCA8Ak*;v+T!PVX=+GVIPvFvQgJy>ThCUtg5%^ z>FIS)-ZPL^EH8;$p&s~Iw~?3{mv{11Q&`ssc>p&(j2rxp$$ z-6@u^sWEoZEw9!hAj;(5?n=O0sOz5kkfL=AU6_y=+)hWthIf_@D*g-DcVq%D#iLZ{ z;k*BUsus|ll}MUAQXC4-@EUVS##WE>pY;Ut7amV(x5&0Jmn z|JNkvCpu!_R#1`0BSCUHUn-sFt`?~=M?c(kXTj@)6j$?>%)laIxOmWrHQneC0?7S4 z?H~7I&UNcRW4!u>xV&!d;Vfd1f~KdyZ5q5;w6%25o^X((%6=3duD6xwi4)B=nRsyN z1Ub_XQ9(^u_-448=UIvO_XiY}Epxh<`KERjyPOilWz?Jgak8m;K)t2uak8yCLk9cA zTi2_;NsSbD98XUbzAfX2+pK?o6kn^mfq#Lfw;C|epo2Yw>Q1Be88J%%^8^Mpe*aHQ z%E*d!Nd9WTp|pm?h7Oy5jmekP(Gz`7Vzo&iDK<1R1O`E~c^`1UuOAQQ10BXnpayw2 z(ZcM@I~_XLmQO4Md1af}{%GE1Hqs#5oru|(Pppgj{qi{2$ttXsU%jQ4hP%u0pa}8Deb-Bmy={wJ`tPd%q%i=}AidFk08MN`_mmB2Nj$aK_{Rzjp(GZ24Fd1`M z2Y7o!qk#~B;r+Sp7|GWFT^1@gG16Vku<~B#LoghaLK8yG3G#Q##j4IZ18@k+lng$s z&><`cSQ?}=KNEp7o0>^5Z`~}tRQ5}3CWp&Jk}&P)hdR7ndJe=rbeN_k#pPRb=MjB5 z4{uGFBtyB~aXP+t<@*c-G`iIxEDCaBy^`q^7?>`vn#a&jk~xY}6xpPis*x}O*0pF_ z%w+W)reTU6QP-NfeMsIy97H)Y^fr5G?)k+GCSN(WBr~cHPq&tU?uH*!gulKSKr)Sg zBl!D?ws5PtLTBbxD{Oa^3}Y*MU|vLeeJl@aD2GjhvQ5+@D)5y8f!#9W-ofl!PDih% zW>_C!v#%VE`1R=wM`#+^LX}tu(NMep8)oH_T?Im$8I!b;H{YA$Ao*YH%`r25{DAJ4 z%Nd{cCWPc>R_}%$%JfbyK)zE`hBl!}6d_C9w*ZXFq{qKl8LbDjFre|#j5xA9ab>t` zTw~aaPo+wW9Rss7T8|ma>eg@>w)p6!La(jy(`r4WFj**HMy9UCXPtC=KPTdl6{hI- ztst~(fB)zK9?ZKKN20(r@pFN(4A}PW64jlS5HhFab0LCzFtbv;bJk{Pu3x9;yG_3? zWX#~-t1^KvS&&snWBZyJi-;a8aTNC!07fkm2p$k-h4vZ}m6UZ#nBS;0Fau#_a zWA;o_^c9e1b8&rze|y`)5(C5_mqFcMpwV8Y_4hog(+siDCMf}tk#%wSD8Ojuuk zjiiL*_MC&S*J+PMAK1Fq7v2p0lMN1`U|B%HPN3SJbsdr@JL`u(#b!kfPNvbmYVKLsTl>k#P$Nn2Zku^A=7@x zrep;i;7KHNwnh)-2IeUdGiQ(ar5uEmmJHvSX$_UBhNwUvn;8CXWso#D9uE`1Rv41g zkgX0AH{qT+IO8L3wU>Va!uVl4hG~4m^{ZE*<&rs>r=>HI7K!j}AREet@pMGVhQ~hW zJu+Oq$veZ}B9twgw5==f$WBClOvu9>N`|2nr?MYnw$ne@p+lXo@|6KJNLRF7g?^8` z=`{2$&90gg#0sZT-}|5WcoZZf#YY{JrLB|IQ4prq%1$qM@ov?^s@EaA!dB^@bfYNQ zr5)zltE|FEEfkI(o@m@gnG}zWo(#&Y%l1XQqUPKA!BUlNWagc$@3FtToJXlI{qA`# zknk?dawygNM7W=Vrbo?^CokhB>#L-xWH{u>zVTPW}irlhDQjkHn;U(opBuFyc2JUCwKp89RcBBdE?#y%JiFLw3@{Sthi~N;R7XZQR8+3v?-aMf3w;u zDS#M91|xyhciC{sYatZoU;O|$Ngi@XBcu@;-olYc1eko?dvFJvXLDWBDP?!atnv2{ zW#j#dUS@cwOm!Zi3kxl&9Ynmi81%EauPDF*4N9KU2B9Z*r@78hu|fm6yosF#42s`< z`FS_Hd$P=5ZD#c-DiVTd8l_JiXaRr_({dWZ_90_ryI8n2(%^iw$I*N2w zn-a^G>g4J97#Q}P(|Kk*{1U^y#<6K^yENC8I9aL6PTqb?tCG6FZPTit&(!-#AQMl& zxDcm?Fgl8YsTwd)9-AUwmXKVKn-54e&-4|l-gDi^*16o6tq_{{ylcY^wE2!XfWcNt zd#i4h!mXFd5T*SiKJ%(a=l#PoRfXegEO76S7}L#yaCyP0>+JrzLA68obAQ2Lo^}4@ zlqmDT?LPCz`@+mk#!WnAZ^p*ZVn-IWNrbrcI5pWk_Z{eMdr=#;#&glVHFxtrmFk6< zI6X;4v--Z?rTf9}RcF!UTc)(kG-#SE*E0xGC(=@S*k{6|r7bX&4x;0)c}dU%y)sxS z@yIUuX0)>0eA>SIPjBvbfMANJt2pDZ?j3y?{7?G(DLY`bwdV8YI^A72MpkvJ>$zO> zb>4A2EJ;{|?jhtju-ZvmpL23HVcjfzjR2@ja-xT6U|GyM4U!X|dj2nOwiWmTq@-cTpF{l&(Xm6J z$QGNq;XJR;D!}_uD)*naaqu(}xEn5G<=dE3cMp;0Gf)Ey*6SaMdV>`z*c4;Su-Lal zERAeQsyHr^d$OIwy6XcHsrF+S92MZ7Wr zHW#_Fx^lEU37SMIV%u-pG)R}cq+}5{RV-hWDgK--o-lGCzVRYXNn?c-*7BvcN+XiW z4bJL`n_Ndj0`p8PET9KT=qvmaeKK zm|GR|zRnt^*0P{9MIX;bCU*AIJVtDVm4jBK;YiWyn@#{ngeGoQF5m6`A$`iw=(ses ztLp_ft!uYkN>GULIq&QJn06&tx+t~y$fvHOf|OANg1FyILz~@;=0;Wv!h%ldo%`Qp z_eWb#{RStm`iAcV;(~`ptK|d@kEo0lZj3D`6YP>hJDqj=-%eM2$~Y&!ybNk&R~yRF z@=T(+GAfGEFVn$c&X4|#83e-OhHE#`JqAQ49G`Ftpe4)r9m0@OA zV<3&u)0KbNBFQ-C&!!<*WI%wzCx@|j`6Kq>Sz~xk&zbm}IF94?b`zbf0I;81gH-bl z*j%nLwrtbUX#FiGq^OZ}%k&6|`$4Njh})!0inT$up!`RHk{s#rPTl7I{8$I6PY{V! zJ!XW0#10VRpww!Jk|mNXWI{rQ+c3q-^JAf17`R-;0+?K=;vIHM@8a&GXfHB??=oVs zd7=l_&loqq-n2N}790-YssBvO4rWN^-qgr}R>HrC*pj4ME9&5dgwP!ckv@$_X~FBi zwSM#GgwUH+C{_4fR+K<6Ab-+Y^=mJsM6_q_bT{C7m#xRmoeRla@b=}QTrNQ6z0CNk?UG;m>VN%B|ddgs$So>~zdc=s+QD&Xg4g)ZZ0dtZa3f~9)4 zNqIj>?5UW69{}L)F}BhdH?{tYl&@rN8xnh8bQP|}fw+hf8s7*0oYpH2&Ls@@mwZ4d zW$ngk!ZANAnquN8&U_TaF}qIi#q~TJf3$Ek!;iE*UhUruKtzaYi4H5XtHrm&#B_yn zlLM<2a_A1{+G}_Qy)(cPjmmg{BHoush#uhmg}v&-&U7Q&al`%=u-qBysy6R7L2Y(4 z9ZCLzHJ1Epq_D45*?37UsFDyaD1&~=!(b<_R=j-|vWadoh-*qs`6fSr##>s?Mm=Af z>ttn{YupWhcL`JudV|$8tNTN#TCM`nBe+pl6tFK5Mtho?3W{~+^4n-N zTeYbZsYp-RdqcmDlAJc+C8|Zy4A%iLhK|eia{qI#i4Gb7Ra|JX=?)6`~vnyA7r`a=fC@!w5Xv zk>>Ce;pX(o|J+Kwm3s4vcCd^Hk-8-#h>J*5jW2pLH~|ziQhcR}79$h7TC!{U-m46u zGq2K}m-ftf@^mM7yaQDyv}3ou$5HjUWiX5(1a*)FKNKFIa&xz&LidAi{>u1Ud#q<- zyYjeyi?b>`jMyZ1VWh|oPyfulhm)cE>jJb=sgiw=rCxa~txml(qR>JmZ)d#DYEF|1 zb;Mwt0q*)-QKD53WP`P9o6|2vMXp9Fk{{9-$>xzz9^Hm&b(!VBvw+^ElIm#y#hXXWA+Tu+Mj1BFG~(3 zp|vTijh6}RFxIh3C{#)W&?@y`Mdxqt&Kez!=_cyyi;&5N-C*_Sdhk$TkmPaJE`tqb zZKpP&_Jh?kg4R7m@ZBaiPs~o6&YGkh$p9tob_z&eIGWJ(p*sjo#s6 zV!Cc^5rwVSAOO1UiWNF&8)$@`%J#r~H+9B?AC~G5zu>R;Qauu1MD0qo^|T{|pD5S$ppF?ymX;W@MF%QGio~LHe7Qks${YD`HT3da4<2L3iReql4i%+$yWa4Gbwn zjf_}v&Lzu|_a@pyikgy_xhg~6uQ%Cd9M%qmIerH~`{NBD@JqN00yE@$e`3vkP5L%l z&gNal`ZdA+bmaNIiepN(Wz!zBsQ3MOJe^SJU@eTKiDEtWymqia*0q(n$mDlBM|%o7E?OOp73O%To>1VXA9-Yl~=%E-WG62G6uh5kpHVPCBOl>>pK9` zdEMW|(9Obduf`uPWfbP%0=t2p{O1V8l;Pq&0kf)qR{^mh`d~IzQqte-i^R>|c7l<& z#)4p;55Wi)Edm-g$G7~hj`jUQ=n0}dZ7G)@G zOOfp?#AO@Rh8x#hDoWm;-iOrfZZ$=@vhm1LzxZEvQ?ot#jNKzlF{UB=*I9l8(NNOL zw|xP#S2!7u8{f$MJruBt08#YVLg=*P7nAukt77=S9=o{iS-BU}Qt2Q<1C^Cb+c}JPQ^_|jPMzQc;WkE%YBaLRtRJEK*;cmE@a>zrH*Va&E zWQ`QFHi+HcaWeh85Bwh#a5 zzj-%1cCP0}jD<*CwZr7&Aci6#j`{xov3Hh1adlC;P69!KCb&aFaM#8m1PJaH+$FfB zaSepv)&zHVNN6CqbO#IWPD5~O-0%5j=9`)O`~JPPe{>bqb(+2FoPG9M>wTZqXLi1b zJI52`k&oUt24=smZbLtpM{V~FCioUUsaPB%Y#d(?J?-s~iQ@e|q3ZMOZIhD?aWO)DY6ya6BtO%Gc4v+$ zVEnu##O5t}>8EC~Y!c_A0(da1{R4OQCcX%3^Cr2!uxpU8AX#!PSA;5@Ne z#kdihcIsS@KVmf|?fLQ4){-yY6(hcp{FaY_;g?tx#=fU2uOsjn5e-;(EZ)0tL3fLb z+&qu`bFf+4Ns_%Q!U)PMCX~CfR-u%Gp>IypgZLmw>lqjkupQlzw)Pvt)BJPV8ghI! z2E8YpEY`FZt~iLDnu_KuO4>f;f*LAjOz!gK-4=CNmKuTTm-gl6{6%j&qsWlc_cUR- ztA90qL!Q1TpIQh-JE4*kZ@m{fuVlKdPEOeg`&SZ-|9lEW7pKt5RvXil8R=gan{$JI+W(Kw5(2ULaIHKn z9GsLyu%ch^-AlDLu6g&dD>~JB$0y7dW2DrYLvgb2!zS?8PicV^*Ig7yvL`}Zp9FXk ziYS?ky8Dkhe(e4ESbq<<0zaAWNXb};{wH~@)j%iempamj&o|tWEB?@Gjxu8*P?q+c zU!>I?(ick9AZmUJYk*OI-z!OgEtxEw&a@I#G2(OdvzqE|7T~lw4LLFsAWv(N%iFs-q`z}LmMh27|(TY2WjxqYxH*<1;2NG zLdy&%Z&E%a%-)#;dJ<&?+j-^ii-}6cawp7Robfm?O0%4@rKe}&mPo#kq5Q`11ZH}$ z%lYyu!#Jm#n?ha(R!yc*G(PmB({FenjJoHg9&8+?<^{Z0^8?HjaZc4ja9eF_=$o?y1 zLgy?rl6`WF)HU%cZo33iCKWDrcgkhqwiAN)cO+ar3Ckr9E$mAsw3pl0Siw@Sc*Y?w zJ2Pxte_02e@iE&5kh{nRkX9N^bc()oHm+?FWUtaAJ|k^4(ub`F&+z$Wf9V}AcL*D%8!%7xLz~T@r@5`ZLnCK{o0Ptca63%}`c@N|w;L=%;^)UC>+J?(bav@h- zyNcF-k|g;27+CDQN3F3tRfL#+_J(YrH7uj^;q9V?ysEW_HKUl|yoKuI^MWKrK=F%< z=9_Q#a5KIV8_??3+!cx#b16s0!3{b%L|l!tScIvJfx=5ViqPTH`mU|4JGPh@tCpox zg}#)`Oty%1fX#fXK8w}U%@?e28AtB*a zwht`Qjt&0FeElWRMEfh@vwJ{_)`!mjc(WT2s#^1A$rd@S3Ua0uZ+u(pH!rrT-|bRZ zRPuVwzjmr=Ooc0oB6c$0&hu3S$sZ)nvO!5#{h#|=ybr<8#=r`1QGHuiQ;2#~u6O*C z6G+0}u(pVnj?X*U8T+se`wp|^?AUo01fB4+v;XX)-h*pTx&CDVTqa8mo}1-(_2@LeaNP-bssV zrmzDJt{1+KSMmit$hIG-jl;GAdW}}Y^Eg*QE``Y)MjBRwrq2dnLAsDQ5tsJYveI0X zX+_L#cY6B2E~mGYPrIM54^?6hqC*<^ASaN9hdMrEA&xTm>npLs$p;JXP_sFjE46Z5c`FT%3Bx6B@+ zE_V~X+n|Me5Y2i2FMGWOoP%A^^TxsNEjR%^B9PWV{}*{%@cH%WJwGYi5ll5Ryawgy z*k(0#Gi{`n*{fkF4XOsn$>@NdY-tdiO>cg%Gx;D|u>pAvc8}~`nWKehZRPTtrKlbX z8}Xjr{t2tD{33E2ag&MgMkcCP1Lkm0@D!Y-jlHi+QYBP&k2uo~2@Ya*9mFgfA+GgE0) zbB4;gR}}HK&GY5V(A=qI|2R>-*Am+AF)aqG)ix4wduB_DO9*i#o0&z|BbCTK zqVM)kL8nhh3SOlzHEiDSw>Sz3Pj4NwSCrq>o{f+Plkr51_%c_(#)rhpk8P05?luYK zr<$iYf3`7OM}9je$V8>ecRUUJ@S2zody-X0Al_K9q<%Vi2E(pXPX zvvZP0v7utj;j$HN9iuYhvdc82tBxiz35y|?3avf!Vk;ojLsY*_DTn)0wZmdl+8B20 zIO<4T%34a7?+Cb7;3Y@Ab?9)V=wQ<-=vdW-_Mg~OXDBj6A%Zo^B;X$98>bYxU^%Cm z*;P4vH?d!2$_q|DHo}S@cfwQKgQBW=fL0ON*w3AlUkV~UWX8jN#&kz zS3<|@1g|Xn6F2K=z)?i3`E~+oi1iD)`yhu!Le>+tc{XXQb^>st^Gc-GK`KiMtAd9I z;WAW8RV~cN`{J2Iinr9LW-B{>(%l)Y-?mav_ zJUOvnmOJ$+qg=0_V-c!lCs>CIxsxdLz1aK_drl$HZ=W0|dA~u1)A9T32Y)<*Yj^w~ zax$CfIp#mJzkYQQO(Q8wat2+gw;Pf<3f@K@SowZHxs6z6?|8bp<s{FX8w!U8bvt=|+?E3kjo5>Zg#<(UxTAJto%l@34 z8J~*XpwUTlB!{NNByz-{RxjYrs#dGQ(D*ilx^a_LXRs?YV*Dn_gEwB)VT9*5c8Y|U zt64nn#g4$}FDyP{W<$Mc=UJu-TKBb8Oz++ROCco7$%lNOBb^$3#+<+<-GNkg=C=yH zPoRPRT>gI@$b=m#wQ6+qZ-bFdetj(GcSIb9OrdNE9OO?1%|3hyvN;Q?GHz43mZcs) zF4ZqchvZcrEjP&7nXPC{&zTfP)&F$5m%X>=*4>Lr<dApOkLbsP8C8xbfWXC|G@5BwmfEz2-<+I?ijkx~6 zw0I+NCF}iG)LRBbu1PrqsQZ*1C)D}pg=9Hxmo#v5M?es!CpDwB})=oUq!wsiNCWC3Xxd-$4_?PvBvUXJ;r?fiMW=7wLVRaq$Qy;0IlX5yo-%=_zZ)pOPUXboR&(*XU*2h?uWu`d| z5ucX7BK?hi-AbIzyQ_V{4zF9ItJ*i1&J&dr8hCa+%(EeOj*fEP{pvTPSb!|eLRVj} znIZduT5AUtC47?8Apx&fL9lS%2i%q^}%mSoNfpJ3h(vEU#*%$|2(vg-EM zP(J5*HDW1!E;T7^8h&CLJ|&2H0b^&WyiQ#2YwYcV?@Z0Y#lhT9sfoksq~*FZ>OY{* z^{BSxDR^~t_aXac{4E6uuWiIS;Ja^i(DBxUyj{-;;(}q3k)b{pnKHKfI@iUYC9**B zE1gn3C-uMH>%Z_RR?6thFJJY}68jw7I)PB+2M5sbC^VH9)$h#TyQlH^6YqXVlvo>{ zZDUlCjU}&9h}jIF0%@fP;(P~CA-nXwn9&2y+qGj{A8`9$FyZ@{l7{2bu=85~ndsjt zBUzrl(7eW>;?_BRq!iJgwiqukuk8d8O`|c5Gi%y(t%4W=pIjOx?+Wr3TZ63vZ>-`OTjJh7CH#4XY{%TIi74xQl6TImn-E3`M3&Wi!;aA8}* zL~3#$zI?Kyr4kA3gDM`kGw|`iiZunw#$(PZ)r!r@X-(+Qoc#RPDc0m#d|GVym!Boh zQl+G2cj<4Yp#D>mCRk+PpL=rlA6$_P;^bv~xUis9w#$VhX?5$k0l_xK%A6#hccKi` zq*1`4YkxHHK7DhJQF}k68p1{Zu?R+d3HG!3g@#8}c9 zg|!F|4Clf`ilWuG;PMC|NBX~Oz2VDBdQE~#8D(he(wz}g&u2*bkL7S8XeQ!h;B&a^ z>%y^f_eVMffl>rMkRyY} ztCRW_vhA#;C#nh#`#PIYhT?~z5MzTM;AZ4cV4Gp)v{SnBggKd+xbHXQh(C*)`+Kl$ zGC+L3E*br^Z#2(v^?vko&^j>T>H$<7rU79%nw>l-`Xq4xuQ4)*VV;7dgj(R@B=Ifl z4=S;aoeEKUM?P7*-nbk-yM*$JtHWb5@M%!sM z4p2vt;b(0*qcwMp`6fFoonviDIUyy~0xfUSCBEEw<59zTV{rSTA3}SVJI}4!sG)u_ zK+&d5^H;b}L4Luhq9*BQ*1X@Kcp)iwe$|4o>;wz7T|={~;E?&C8+)w^mdybOjJ{Tl z(S=+My5yE+=;mr*w^X0kXoefjS>0-{RZ|XIn8kcxe`TEr?Nxj}6j@-|Pg)gsXNW0X zH07~bsz;XTD?UIL^ED>Y&guCKU!-Hppfdx9ZZ&wh4JJDXCITpF2me9`@b9m zt-Z%SVV$Wb!Ekkq{IlJr|Xc=ZL;=jCeyQC7dU8!rUGTPK3l%SIO z1BKJ+*^tk9z{~7=NzNc;AHMnpb3g4+Y$6t|C-)t)@DJDZMH@Un&i_)>jLAn+H%uKr zamh5CY~&{vIrCz$%K%O97=4<|3TUl4JU{8`wvVrF%uyk* z#JWT&v@m9Tk6QFM{r8)g2TsYSDt1D?`geAoNO8Uq(+A*U8a+PPS3LpcIz@@mWD?5I z=5&5LR3lBidlQYn?1sgNJUZLL5(qUvIn)ru9Z&5e`a5-ci+}t?l5;Aoe;Z{|da=@i zBw97wfF6fEx99611(PECXii@gi6&seilr*`#oYM&`XFwB8@8QdB?et_!-2W+QcY;H z>PkMyxD9JRCX{i3o4Dp;T@{=a<+k7ZgaSzSLR~3}G~zTLNW}Tfm+JE8{0~r$)oad-$y3FU_#2#1{du~evv}6*cNG~)s6eQ42;UQ1Kdy~IlKevtn9!(@{1r6( zwI@YbMMs9PNuE_%E2*9YBMO?6)L4|XMXpO%NIHyW9PVRp{(5~NgY|p*RE4cLkwi*F zwq56d1a*Z=^|zgezb|&a4RQcZy>H9W94ffXdP`$p3!p)V>*KT3_EYs`EZVObyfa1R zu~NdaW4Z?R4Vg|~sD*8szIRCxbG)H^6d-GSSvt9otvvWQ#^asEwIA1OQipDY7ae0F z5Z>*fDp03<3GVC^jxzfdX8e@AJdf@HGx~>HMr<&67LDBR?3?GVOCHh*bv-j)DJ>BW zJj?8jc`tT^a9WT;%#aZ7lBZYAZ|&TAf4t%1ILJ=Ij(Lmbq<);+I$p$l{6sR+?N>w| z?Yt@pBMu4qBp(Oqc6Q0-*9B&+p#c5U@RpQw!>2Djp?nxuwF0ds(4k z5*P(&E+&UdyX~3bu$;wUMD3zD-4b>(W-QHk63sQXmD%=3lR5API2WGU1s(=58g-9j zS&3SUx1ms?EY53^p)^FUz=Z5dxQ888$aVSNkyf@_PRglb!aqul4mvXU1UWZ9xE(Cm z@mlu`Hjw%W6~eq*wM1`GO;HJP)ZUgo~@Ko5-^kxbJ#Bl;uRll%{M@1VfgmCs#*uZLpI zN)TO(sojgvLl*G-4q4$=IMNOcv>>x=Pto~_JP|@tfB>*9n@x7hXNE#FQB1p=!0GRb zZw1FuY4)o6F;5+CY{$DVcQPknKm2pE+k=-2 z1t$Zn<0lCf73R%MMVC@Rj)5EYM)N4E0u|0ksVb|zz zQeGPT8!SpLkSevRTV;$#a;W3rJ3PgULSi;08yZPKP9&x~;_=))!;*XP#sW4H&s8}D z3Z_xXvK-nxuz{FFMudm=w|Hjo$wOI8gSgbj2(k&7B#8BDoE-gG`Z|1Y$AVQr)i)q1 zw<%sC17odl$|e!h?b&Z%Uj*@uf|#O-+%Drfbn%D>HHA*&!U-9_BGtfJVV-7|lR*jZ zXMANU2LNUNVA$bi9f3`0o=ic+w+^A!Hv(nfUlp0J_V)I-D($@b?od!fmP{OjH4P`S zEK7X5F-*O(JrK8ZrgDs}IZ+(rk@&_bn~}CvaCDG|YQdGyuZc$Wt&B4o2bbd$2d>BT0-F2u;lr?T3+DP=P<3lf#g;Y;FS9YF);J&jDif0 z^g-4g4pfmz!33`v8Ju(BrW86+nc(tHUtUL;D8F%2pTa|;4Y7R>FYi--ISY6CM!icE z605}4>3+hX3DfuqMJ>GSxN{09lZl3@t(pKSxQHawpCTfKe`|CoxpH;vtB5hu@U}3?KqjzZ(Ol7khUi7Sdd^h5zK}5Gh7YG zYc!Qq`>)ajK0SqP8?%ql3~-UyCYv=PXj|hA@wr?aTw2uwzQg-4u5WT;N&06odB>^s zOc$=y&~fHiSGBVu-#iz^OToMO>XUunjZXO+k31}_ge3al@7%lCN8qvkl(}HC$gmZ1 zz|=navBQ|^{9BT4VKDl(GRg5WAm?A)ki5W^k~U?&)7m%0JjQ>GPB_t2zPX{y`5X`V z2WApw9aDJHjH6ZNmh1d&DOze#YN?km2;|aqUiTtGwwVWZR$A@QQc_k3{viDHTWr(L zjkT%K+52N=MwYMLdU>N?hULGad=FkgKf{h5Z(yRxOD->)2||h8lovTc-r=O&5UH_c zioQyKu|MrTzC$PC-dUA8bh9#_e6nuoOCO2u+*3%;fX-shV$vPmz3af=xCahOUnXkV z6wDuH{=VAe!xBv7EzKmK&A6s%ZP+zkV`89Nr2q;5GiAhGOqUwt*i=F~C40zTj4C4r za2X-Y`tQ^E>`Dn(QkZlLMBzP{CzPuNUAp+P6xY3t$8lCbezOD}UY6OEqE~54XLO<- zY<2dh`N*;pe!Z{6GwY#Z_8!eY#8m6FsrIPNHSA}qq2?}L6AGiS|0L- z)60xCdO?eCdwXZqjE`JFE7Rk^-P~d!-^G^i?OXW~b~R%KLQa-9wJ$cka|VTf&NvdX zxHX2d?yhq!+uEjfdZ9r5x0muy`W!(QSh}m4+?ej;TZP^1%H%HliqxW3IdxPU?(e~+ z0~%H7Qz_IJC~dK4)=u3*bmRgF32XKaCz20Exe*I;vUs=NbXOgy=zxAmkKms8nSQ?D zcx!59L~s(Roq}*;v~X(azWcAgPZ(YzzoNtDR7K;(aM4DdIpoFL0A}5BUe(Mn5Sim|#Y%-+xaJpyw$?mR9Zfc!E%C`Ysj`tv#?f#!WArYUNAz#@zf|zdCW8*qJTdqVE zavq{O|KZ4VkFnc#-hZ3Pog${H)n!PC_*(R15y5EB+U(zQ$=Y#^z3Fj8lvAP~yCidH zzq(JV-1^4!c!pNbzVT*{-J~&^oHiMgyeE052o$;@@k?&5rU)icu1U5kW#AJlFD-PO zv+x@$l}(eXv(AJv9BxaRD)RXdcA;jqBy9f6d-i+MO>+#nIre%yzvWlQs%I$Q!pX$c zCQbNQ6fBN^j}mE-ZLnbIGKTV@c_U3|dbo-anZWao6ANy88^xjK{hE{5tgHJ#$BEmO zE&h=*RP4GMK-=n6hvEg++@XzVdHt&^s~V! zA)aoUl+!(==y|s_TF(t=)Ekrx!_no2V+A)oJ3`qL@C&0r7vv?vXWW!<0o9)Jr|!^O zaCzZ`^XgHjB+U5lvv0+qL#(LrMBHf}vPGOVq-j~ftTf=F4NEzblS=fVI>{eohr9(* zlF8gE_KO368^skij(5N~;paTmdM2xFUHVkL{p_gZ9)?2h^?!Tep`nIm1lfTb5rWmdtMFqx z>O=4NWLGqinp{CC+c!ADd<2jMGe1|#%e9^7b;8lsRan}VopElJDS%gG()Q#uSSUc; zjxEI|Jk^dED%|E0%?oyYRRhI7jn)lgk4}-mI~XtcT~f7eW?sv)j9m6`rV4i5Of%1; zv@B%grBs?}`C!5l7aeYTg_(nAOhmm4WvA+PR~*>Y5*9tMA(K#AC4H?rer@RjV_Y|* zrHc05G4nA^7hD+t4?bEcNqZ@?@bp-1IxhO2yvot7#zC(k78JgxejzceWN|$HNz}&2 z#rT>;Y?)`d3EDdLfxLAmpp~*_hx7F8uffu^&LCE01o)orXI9hN(||p}VT-`$#rkDh zEzzoF{kt+eWM_?C>t5&39uw%;2g+&Vo=@JQg(L2g*g&;NS!a1(iWF8gWHOk9CbRcX zx;EvXqUS)g4}}NhhzbOOWPPH2Fl~y}Em_ZH8OiGcx8Ah!Wn;w)mJ&<)P^vve+A;!*J~Lnrj-(xb47nuA^|Q5FQvRLj8~n3R#`{ zc1(bs?ainWH@4B|`Pxpu%OLwQoh$~8AbPs<$B>Jy@Yz@0Zl2JeH;X_Xic!c^_LBMX z302&7XBP$7c{9dz86B4|6w_-MJBQ=~%mp93To(qATvE`tyZnB6>)aKFZlH`FlZ`U*DzbH(e{)tVX8m;XfrRxS^dJWAc6UI%RX$> zl@8YDdSplZ4M<~BKwi-*I81Q~IfbdBh97sWh7><%^9t#rta+#$u!N-Qw@eYSJ-(l) z?>6&Z{>0S^AH-uL0RF|$B6LMw@MllT-;TH&v&4ETFP)q;*F`4K26rYKhp@k7?!p(l zEqKfi>TVaySEdl4)&CgBM;o1h2QeYW21)PL^5eiG?@dYgaN2Zs=6Y1mzhA=7M0rU1 z-F0eMjtKf+qm!)vsm+tDgr!~b&HuXGKNpQBLI*^-M4t__|NnRYU)LcxKGP{>WJ^&l zO^zP#AqzLygl3FDjvRPi9-XyE9^PMxjd;32)Lnl7Kmv=Od*i}sKowr{PDoe%(QwH z#wt$A%BR(e_6(tf=p8oIjk57nC57Pd1?a&HIPg;iM@;4A+}!FNmRgh(6BG5{yXv(= zqXku;q6COORAGCNa-iUOsS4xLh_fv;IwpmM^$b)=i?1mj#GBdG%x%M{VF8fz$s zU{GadS98UGPos10cQ7S)(Bx@oq!8eo55GH!5IsA7gX|?x?XO+yc?&cQcg){HbqaR< zb?FmJ3YGUoz+&pndSHCgJwWTzUK{Xt7-$U}Ld_JOQlTS1S8oMWzTf&d*BT4kYUl~| zNShXOQ-WO7T0H#~18Dg4j@r14MTgFoDgF#+2o46)_!|x+Gr!R+R#K;gY&qghM2jEN zQj7U$odDc`y!3gq@B+q;>_2(6EvdsGTiLuj7;N~7keIE?`mHa=2Hn6 z9x}=UnQaCB36^{bC}jR~Q&B z=#3=UOtZ;O%<{$k{c!KQ*)8B`IXGR_xuon9aGp}RQY*DPRly2~U&aylc$Q<`GNl%I zeph4gSDK=-(^W={kG|Jd`VH1qeBzgeLg3?gzgr_tI{>5aF#ki{spDQHO(L*;D4kys z4f{%&MZ26H$atoj^@I;9KHlcsU2o|zYn8@pRqCn7ppoM*HUf~RdY?oHy8**d;Bg)y z`$w&Etui(fzmp>D2zZ6nAo0MT!=v-~G+t{30DSY7p8kFD$agy3&+XE?ljU?Lm?z_Z zL$GS8Qt6F;wY>%4J_-A??V3QQ{`J_{SiSct@%7y)=f?e|M6&*>att~Ds{cIa<>GB% zz5AAzen-F??7VqRu?<778ondX!u z_ITGYUi0Y81JJlg$*eXr!()n+%3h5>5Vjqo zq~6C}=$43L<$58nD8tc~pJzvp6@<(hTG@ez`e}T2^_0RMYCZwiLc0rZ)z!zw+x>mZ zaC_DPG7m&m;$9CYtxU6|-zwFJC9CF_O1gmEYsYy*TA=l=@@Z#$n&rpK&nu9E<3EI~ zI)q@3MD1B)M59hBN6on@>G8+I;x?Y0^xx{r?K3@p*p}z3xSf@7>aD z&e`PXX7cUNasVR3f5#sN+<$EPd*84Xh0^o=u&}UZSldL;{D7r-4>jlp8eJISFdXu4 zGSk0&nb3Wf{&TN~fL9JsiNx{>)C>~o8gc-4PFUWk7SQH_lf=j)q0#_q3}H87GVyze zGz|2&*_o<4K1WSqQ-4JrbAyEK)nLXjg_D^085;wd4X>l+Dn70(19t$vmspY$ zpuy6!*TZGjjmoTDUbwy?W+#{tWRpJrY&%=frajv=pYCLzcf@uYXxg%QcRDh21w4^f z=jBu6M=q|uZLes=ln&;NOD_eN!6?<<8@J0V@(sl?W|>s)3^G29WSeZKm1H=zsvG;9 z2$5>sCRZ0lZ7C`#VYk{G{J7KBH7YYME!?1QuFYCm<&)-PV^g|T>C6dykXa6>$Y|W6 zaA@Oq7=4mm8#10y0^Z7PtocbEh^gHj_HT8omH)bKjk^ zlANiMnis@OxjSbdmqg-~fJk_+NvWw68Q-yP^X;LC71tk_N66<;8_QS6KL}5@2Dca! z&@0IRP=*I7V{mo@WNl6U3sP6@= z|GJ7(%kN7RVWM$cN5t5;z_9eQTXZ@ZD2y~JNF-(rp!4kFw47G^TCR4kxZ?Yj8u}X9 zkUA6bt?ZTnDsaXwT$&4LM7Z*^b&?*v8%#7sTiepJoe8<>raL!ql2-*|v{h_(r=<{D z4cvOA-9P;=Ki7Q#OG5qTfE{%Vb@*$U7g#Y2K9fELj?Xo2-}5fT{m(m7zc&~VeQzYK zFyRY9$C@>o?b(}24HP@@$myjca3`v=FMT~`4$?6)r_6G7*MB*8On{{_>*m`1BeECD zjN4jTr#Fd|A8}-R>k8oXuSE^R@hI{Ql=$tnW<;GFt3o1*d@nDH^iLtXbouC3j}Iv6 zBPbo%@Ypq~_g5CHUMb!l-DW>VCnhNu)QSdlo;q^#pxAgeX@8#5P903{CvcH${=#Rs z`2O!&G=2;TZ=o$%t5U;gd3Rcy!9v7tqg4G{hoN^zp6da>?jU21AN^7LDP+-aLfL)@ z+-BK0*85|8mdC=hRTCb2<3xiDSBiz(3uy)?^AZu3=QdTEU43S1JC=ua5g-wd+;GLI`={(H;?UVFr@ zv{s=CB zFOnAMb4prnYfpML`|vGJHQo)}n?+~x^)ZrpJVmf(MYKS0U_Fm64|LZmX*N=3Abr^J zXeQRw>M^I$d=4=E=RcQ37RS{2bm|A6NK2PG>?FFusXq6kgPQyxxv(9t5=p-nm6CZuLQKx{=(}UZjKMj}4qarkQb-*?5TY(U zM0>M0y>OVkbkm#q$ovcF&?7&1$`Sv`)b^zAA;Y;{ejvGmMbK0+-X?C;=OC)JOtt+> zPmm>34CCr{jPJ>pttsC=UKP*1X;*66&DdL4N|*l_APRt;L*Q|PorQyokAs8iW4f(; zLLtMw+g#h)6G7m)7qJy`6+qiP3n8XCh=mL2Vv0r*0lT6sCKS9)w3cVGzLjU=B;-06 zV->mZOAN+X@ioIcY%AE*iFFL)wc*1gV-5y9eB%8&KQ4ymb9mQ8-+5c+yDbG!s zoL`uQ2%hODW;Bm6k)*JhCv0&2+;AL<-xhHabVrw9PHBBwt(L2nC*!7ZjT62(;BTQVpQ;p znV-JVv_Pn zS%VF6YTD2C4qq5=y{v5h}1;@Abv(y$K74P<);Ej5uYLe7j{`K!c6nG{Z z-By!HC2c5p?Q%z_im8cvue&t<*9#UPfSBNdP?#ca7+ij_MvMyc{d*Sw=d&aT<&U=O zi|@N?|8rgf;btyVx}c5jEJ?%)|M!AD1>B&W|HQM;iiMN^uS*C5%Hb#LmnE%n`Z>$P z54V$4HI74qq`w~SKdMx|S3!)}`7us^)5J?z^0I>ZS+mnGX9Cn3aAhWtVW<+y%3kua zf!>wbj-1%?h@)4CG}P;-V1_etv9q=Y*S3z718$1vJxTf>b*^|>5z3e2@kWNmtv2sO z;|1sJl1OGE*ZV6aRM2K3r_(fDgv_7R<$RjrY$L#<0;yU2FpN7!c6!C8Fx~-+PX=K z7k0jm38B$jHU*}++vlI};XMf68(_)u!=NF8s3V1 zrFqn5r9$t-2X?J9ti7pMfVPYh1Xi%^Ve07C{varO1amQl%$WYB2uKT3eNB<`3@H14 zysdiZ^kAjI09FGuh>WwkN+qiX{tzf^*x>ktY~p$oso(IokUN;l9Q$?j^QcpM*KYkx zmu25#3Y04BG%B_%t2Qv(VV@i+7R1TL%Ss&KfU>s^*X($Fkh^aUn^Goa9r^&!C^(yyHO9fD%DF+&JOZni946{@@XTMxh1spI*ns2o>kp^AT!gl>0y z@E>-(XT0QG1{js_@wTS6a|7`jJFBsHM0a5~1y1mles)(zHD;Ne1Ioh1 z#K`?dnvKciNDSA?yrXWhx82G5CgTU}*mF{LHjB#-DxXO9EV%$4NahZk^mhR2H~^e% zCz8WSU;ENM03#*tN}8MR7y}^ zV;qhKFMPq!lm;GSFa@I`gd=GskY=AVXhpJRZsqRy_#08h!}vQ0BZ{#u;9z?oJ?VAip9LckyNYI@fd|pvBTof<7g$qw$HelzRoD94=;qhN- z2Y7jZ9r|QuJ1@A0w=M3my?%mDj-^Kc@~$S8^^S2%8cfpg?i6h~NfmYqbbKuGA4zCx ztG^&wMqPbvT*UeFMZwg(sb@(^2~?=cfyH%)YX`ac#jL%nkXZhgUJD7#)|7l<6K9Bb z*&vhaZuPQu?ak)$Ep>w96^y}5{C;22`##1=xH|Gpt+J8@6~qag_BTbYKGqJ++4Yx= z_js+#n)%!hW*r2Y#pytv^hx?3;Y#sty-Z!!0-{Z%DxhBZ^qUxCb7tjY`beV==Z=~v z4e`iiXY9yj=1ue6>9Umve1wxt<=BK_u@gRV=xs%-*=83_QS~B-rUH&-lhws{YpkoM zN7|mF{o$mpa;+Yz6onBtdgM+e?ng?aaBtW#)^|p*iQ-B&EUVh?7kEG&aQ;vb*Aey( z$@)|Bu5x0aq0N|_{ZGZu0}Sz@`SU6~Eunh0jx^Y6UmoTaChQZUjO5aF@Es0$G>ITW z2`c)_XTj{8i<4`~WiF`r9Syg6wCLr2(l*88=jmDTPaR{_dWBzvW zSmL;?wa{ORrSO@7SO(@e-8~T#`VbyA+lXY( zR!Po{BHiz^XtIWljyUlcFPNh7hQU&p)uISBTb!On&5>~%Z`sD ziCHB_McifZ0bQ(-o~8f2GUt~qv7NVU3-;jePFG-@R>2mOou3};K{P%69)qVa$4uZZ zx#P(BV(Ouq4$>#i9U;R;M^qv*oIBv_Z-Su%(tA+uz*quuU2L6jZ)36F7ds$X;n4Fb zYQLiz;51;j@|5o*rU{TdSUo-MgFPGk(`~SC(>ZEJtI`(*>PWkq5^NjC&h5OYQ$ave zRi|+afT%hKxfdk$ZDqCVNt=xj8pMqHaX(vkIq=?-)6arOWx|d!mQ?5?;%@Cbt&(XD zd6iwz*IL|yBr8&=56Z;7qvFh(@zu~Vcp`G-*hV(A3&5PCh&t&(f5O+pfOC5WaLA+a z-svkTViU(uVML8NfYbQC2RU=={fZ_o7#pvuiKCPc5xtPjVf@r=A*accjH8nsgn+5W z1CikMpd>XZlc2NL-qYoqqZqHh`fbMTyjhpcR7g*$-zr70p|-2sEm@3fnFEewZM~C9 z{RW+?Ga&a;U@@?cD7^kj(0p_6xJs+vY+jVyJu`A$H{yNz7qNGkVRcq2aV}lj7X|+f z=JMRGeJRYX%Va|RZTT^H1YjfoK7RRT4O57Ib1+pi?`u?)=4^jb36nUaBp!%tsY9J| ztBXrL3JMB5~ zHqN(-$!cr)sW#oc@>j3?^)WGV67CYN&ru3bw??@dEAK57#7QFpZDZ zy`ih?^AWu7NawrAV;JT@@*&D%@T;{yaDnz#-#N zl34u+C@PuC>FMGWLZOsT?w%aHAJwbZlE(}-rwGBc-Jd;Sao8M4nbMw8IvbNYm}fd- zFg2$JYINgL?V2viT>6hdC)^Zqq%ihb$}@k%v^qQs3utb2e-ljMx!)^#LRdI-U;KyT z#&7O-n<{U*d@I&*qqAA8ls2wWrXiD;TK`znv7?ZjL@ZG{SnEAVe2s`( ztN53DM-ScTL<2+gqoo{)lT_uv@@$P9g79MXCr& zJj#Y-iFWK{51!fHAE<5%4O^c-a29#{xeM=5A1_E6NhLxP5?-=UXl;Z>+~qCvTu?FY z0jWFnfb9vIbCN^_BG!RSp;KB!-n2)FIRY#lw-r-{CKE6AH zC(P7U6Z*A7rz(BE1c?1LIX43)Q^I648myJIa%!<}KM@^ka%$ktNJVg%w7;^e*i?;n zjAh;BK^vfgzgdu4QWaC7>V2T6svEtf6d__so@pG5;&xsZrCMS5HF2EP$5HcH?l!T= zcuBZhWwotSwXD|}ZTRqc2be}9{dh(g5G_r#et zaO_I2BfzG5Kka|UPF$aovD1#a78f60oZ zfS*Q{JyB7lKkq7ZCG?NUeJ;Y5&#?y=6V9}SmDFV!{bh3yBLDO_%>DUowhtc};>)kM zlqi$x$#TmiPmMMR1`RqsAsVhN?!=$?(h<|VkBP=W&$7CACvbV3>T^jXs(;;5A}%X1Zqff&C8)YAy(dlTazd^{3ydKr|9A{T4y)WD1Jk}S3%dsD{#1F_tCyA&9FH%is*Mz>AC4NnGo_aC>|IRzmSgYE)I>eyB~9v$$F^do5%UdT z-4k?p@*n_|6I4BO3ASy^qaM`m=ZIqIv>%-|tkYxLArvBhXvTt$$zZWm zoN;0&iR3T1%W__u`DkhVz*V>)1H^#T%#1R5-}fxmA6HnNx2GAbE;dtFl0T3#1M2vi z9g)x7o#kp$_A`|%+7;SU{q{J7563TeD*lK|JmfFf^D+IAcogk3=?D-zSfCaYPk_Zx zbkW8pJ2`vmLV;~GqHDOb|6-?vS~}-QOMKY}E?rLBDay&>v6m8lCLznr5iO$gvgLgDcLQfDhhi>0nfPNq z;01lxfg<;E(~{;5f+Yk zaEqE{e~-k1F-@qG@@i=z&&+9su{~G)DA41gXe92QYYc-SG7m{T;I6?QjCqL1#sskP z^L;GF3(JHfYlj3^&WL>!|2TVQ`08p zUp6@M(_^IM$Zfwu8ie$K_OL9hD*5bZ)zD1c3JhHq-Rc8>E^L)cfab(6uH1oS^4qP+ z3TEv(Ko@OOD~H_tuajC}JPi!o zkdmePD&y6v2(2lv0?AEgFg*=^?&rlk2E`YM%c*OUuiwfi1Hwx6kgd}tWy&{O?vWzFU_!=CnhhX%3a=frxx$y^X9BXO8IZ z!|#ge1tWOWqVy&}C2Myd@Vg?&t6?wN77e+=!^6uyf0tF6sb2DQl@X#nFn~I00tAjt z&nz3ghxNzBLU*G7L)=?N#g#?d!nixZr69Pwy9a^>f;%BVaCdhIlHhK^g1fuB1_)ZX z1gXLc{i^Tl?%U%V`p)W=~6sA+&c3ja`~*X>Yl9Y5tk68J6O3;(0owU(L4k4+a3WDa+I^IBd-B)PO(e;hlDZJr>I z!h)hukni)o{Bex8p0UM$ z@&-JCt-8hY;o<&-Zf}dchQxP6vDQb^;>TLFd6y^e>$*~Z-QfHDcYZpC?nMbVbr|&C zTTs}x%86LiXNb1gZ&+z0vVpDpThTCMo@vzmiGbbNz{#utX6K*PGox^ZIM4~B7{#k= zoa4~=N3;Zj9~Z|~#xPG(TV1brQO0%cpqMw7ZvS`zY`EXSuR0JTjU&GmirGhoxA8-= zWbIu`8tzd5_>M=y7U`iAvE6oob|#P_ythnp>wCKgK83fNC?_Fael?p&t?IU|?)n|6 zXmr6<_julYCx8^ zOW>0V$1f6n@cX`ylx5x=34jd!Z%1URn!2M3k28)tHkd8qcv`w+_r0>Y?n9GzhcmTi zG=9kh5nayC))xT)(ZcSE>b9}SDtrKjxXH^~_r?u7vJqGtFL1GrKd2cpdXed_|KP;m zANh-GSU+UvEygRWF1axaU|Jq>9thOf`UMoQ%-r_@>om_W;wP^M1JhzpmZj_>BdSkgmx)kT;RC(xjTpN`Ub7@Co@Nu0D#f& z7r2tASh)Dde3334pll0add}mYUtLan4N4%B9jBMmKa-Gxh*ng8xxK7=-#uF&B6S6x z93Orx1xHw{4+{~p*tqjtWI6gmI5X~nzrjYH&-ToK(-Ca(`@y>xWB@*`%K0|p^@iV{ z4Q6!Z$Yrg^tVRj&UW4#Uo$jRuvaZ*0V9mn|xd^32VBW=W*16X6fT$OYUkeNo{0QlE z8so*vRt|fYw)_3wn5MsD&sAcioMfqES(EeG=|in>{ikpygf;TCX*jGRCSSv z3xv$E-}_q~=Xh4dZ2xU72ZK5q}o zjuN^Lz@C-97d9?71tCE;14MG}$T|4$T)KTh1tTfOC&?q6kZr5l@%}PoNfN-Re$n9QJUlw$ zq5r(t*YkYa(ZABVkVXEWcyU+N9ngb$6GtgBmX;_SAyl)!Udm~DKeuBE^pNihdGU8X zhn=5F_oCI%8JjZT{YZ>cQem=5I|xv96*!luHNm13q~!tT5K9E$u@0a>dLE%A`Vw^I$+P=VCcQuW!t@8 zs`bA%oC>3hQ~bd8M}e|kQ^(VNoBjngmwJBm<676-57kIf^cv`YB0uzQPrenDts~~;#X5U7b_U;^!Xn{QOuX{K zS(&L=0nP-YuEdZDyM^&utDCftT)~wJsM9lRvmz&`e5GG-l}Q*9pPjBsMO=y44i;Iu z>V4g)=G?RT6c%_hdAZqjKm$6j^~bnYK8;(eI5(|^Yr|v6d-qraAF#}(=wRY(8S7kN zc`rj$gShkAH0!9?%rxhkSx*p7yjrh8b^n`a*ZJxM9oo4)Qan1+IJZs%A+t{Tk3)`p z(M1ud8U~|hL^RnyO6{ndh#@bFqtT*DV5kLyhxLi@^KroLJ=8h@;Tyizx4j7_sSa$8K!v zUE3<-;?x@cG=am-CnPj`1nhdhK>KHsPAM7hP?f-5PJ?FFkk}+myrp={Ljm!%*cqFYf5LG34mkn&3DWQ9>!9?n0k!Fp0hoZK zSE&xd67s#Sz_Cou!J-SQpk{xVnb~RUAjP+gM;Jy4{2hG=NSeAQTIGAj zG{sT~uQsv*}MT+)rjH=CVG3c52V z{!tkwJ#lu~hHv2-E`K+`!#sT+z zSUT2P>Kg!@-ZL9HyZo$_k|=u5q)TO@Y_jwYbT(vxbj}3#&{LzPI%iJEtQFK$+!GxQ zBF@SXS@u@AJbF%zMRNw&U(Zdo_L~s30bdxt$NK}S zcB!;`X?_up1r?M)#pUhm8!LQ;w{|CG!&za{19(nZ9}KrE!V(OSc6d_JM!mhbLzVb} zt5q7nLxRJZUo;JM=zt+JFO7iD=j(MuW(I4s8NB0NbosqP8H4q@&w3>NuF$b_B(-~^6n3x~2)&@Eg6qk}6xPn~>6W_C9LIFi6@x8-q|7-sU`P%1fpU@@B_m{y+-evAhW3 z04rgWfWsTq~>Rs2YT~ zh@%78froeY*kmW8qc87|5}>`fiuD4Olgy7a(*|H^&iluhk#diGC9WznbOu!-zWf>w+o%Y+h;V-ydQuGTI0C z+I27}4l=-m&WJen(82ROn0^kBf{pir1q!(K5n}*5r9#{1FF$a ztxkBW@@2g&I9)b7{WIRV1oICVE7SbWKe*DOetAGg=dX`gwm&{S{ciB&c(glBiiTH% zb(*J2cYX%Ka?lWqoWdjYO=Y!w$XRWo9ou>ZFdH-{qmY8c9zdaYbd)TGyo_r3EQfRc zMExj}!sJs22fi+9Ko;aYsLyNcd+^7KrAvfmCF7?hM^LFMt6(AJeQ|FdnuWQ3?&Rz= zA)E0xZrzQDKQfWnuKurE;-93~aDfR8K2t~@(`T%^>q}K@x>0oy_P*?HYw!ntqJ+J= z3;ScoRl?*U|Dac@Rk~&6Uk{UVm*|KeIIDXl4vVlw-f#Z3k%=R@^CQ9dFLpLa2IgQYhj>3O3yT$A^a2Hp-+KI$Vx`_nbB%{KFK0YB%Sdagxy9_!S2#+>94Wrws z2^P2;iq!$h3d%K7Tg@0n9itEoC={vpz}x*{Tt!2Zb6c@k$s8UwpT9J2;pk zt$USH`!breI=j6!-wP{dc`4+_D@hrqlGQR1Q~ToIKK}wANt^d3{-rMquF=u|0mGC{ z)$Eq-A(US2UgN_|LsBW0l9z9nALM6KnJ6#7%||YqeW4Brx4qr6^;tX-nTVY=Sv6NQ zhYuIgn_x-nWUHm4u?fPWQl{2Jz1V$bd-|g_4!M}tZsJw>bh~+sBy>G=4<|tpuWzP? zRwSpIUb`jM<4;#Y#C)^Lm3QwrO~~G4M#CqVhsz1GO4JG>qY{L*6W_TNKu-#?-+h#$ z9twcn(JYI<8u;*bpA7@A(6}5%BJ8~JOt^btw zdB#YY$4dDP!;QJz6@HePCGf@X-jotVItIMRX&~OcNwTJRygi|H)WR9fj}8Xvm*G4P zv5|AY{~@bsNksi~g-bIQxz$buw3r`~w0CXnnSRbEYH6s(=$_^6%S}j&IQCNuB<4wd z+_lG@-cAY^=#6EaJ#vfi*w`g_i-I#;V~qN3?9wt|?fiEI;I3Th`m8Lk+;aw7_kEVk zY85>1(-cVbZ6`$D797|*n#9)lIdWzB)6PAhT$#!3;X`iGC#bCDNIUK z1jo?W_6DsfpaI=~epd_}8Hz(VMl91&0Ee8Nvb7gXDNfj1(#^LV+bJXF{*<6X*edvkjq(Mle%G^AzN4wz3vlcq1$az4(5~N~ zC`mJGY3eu8DgLo9)bZzg0DLCY^QOpi^HCQ;oE0ry?2beKW%=~Ts`oZK_>Sf2RHh?7 zUsURyUYlQVW#zT0%1EP$)q>iSA+@B;hp=a8k%K!Zfb=nEaC?Lq0l0a6^eb=p^|H$} zPELR{|E6{H%pbBJ^P~(JyGJlH8=ZC3oh6}t)_;@;`z1fM`6Hpdkto<>76^Ok5^iNN zzo=KJ7m}dLd@eAH6B-E#WG9S#67#)X$kto1S!^+>tx-kHX?|J4ztFb_1n&nW&Aj95l5PJ+ztVo+mti#|q z4@JgK0-G&jCt3G(+Po$1k1X?@5$ApA__mmvf_!o5hbe`3wd`gloCc+OnKMBrEYFRgn9!xi9t5vo|H2 zI^O3u_Fg;r0w_r^n-6||okMeI@$sN*q<6u=!A~Wo1{rWSB;*VE;{!hsFcHWt3J8Z` zO17cp!_MrB+Vxf_lXn6k?jzZ&-3I-Ms-p*SNKyqQ@)yh!f3kT1RhNZRe|`iiEFL$Klwf6{E0BIWc;F|y;qDX) zEDMSxe#IypoGOZmH5nzGwGtjTa;WYJ5wl` zI0!>}-l2n7#-<0tPQd)xzBQB+SxJg_4VO_~l^!S<7~fm`XGeOG0_uLk{4`LsveFez zOn-x0nJzUOzJ$U=^>!hc z^un#8vclfpfaAH6La^w~JI!`gY6L|x`ytr=ES5?FHa+G)6gtn$Bqg_GdWE!Q!Y-9? z%X0WAE9z^w^slGvSD`;zDmKuh3+EJI{^d7%X|u; zE};_5%dgVMcXV`QN)S*4g+-%hid~~tdW6DsMm`d0i|t$X;Z=y^RN9Y-$pbWg2{v+a z+V516&%m6>1O_Q0bncBM8JrBdo}Zqw-jfTtXk`!fT|pbY7T4_N0)Gd|!`&_9@WJ8` zHac{6?QBwaL8&v+=+CfssL~`miJ$lHL|ZU9K0DzknDjPO&QR1UY*|U*aiQj4NSaD- zjkrcDmfbPHAB}Krjd}$i;_4G2RD$}}gg~KssP;khSQe3&vAzQ>O2Y-vUGc=5bBsi0EL5W#_Yl zVnx09IS-01w%3H5-gsEq4)Rf!a!S!qNutAWn@P@SzuD75)hp$=kQu81zpSl5TIpyK+k zj+{cF5tp$q5nSlcVb7zV2wYIlIaUFt)qV-#6@b8d?T5rv__KQqeG?oGip}yg1#ifKny>@n!Ya=W?OGx@xx-(VYJ0BtshH!H>Nb?CJIe*-kdQ&0YdDw47 zjeVXeI{1ZgyBy{DCeq7SKT1Y&AqhDiv zVCS_b71@IGp6P46^;*lEMy|rI8r~I1me0*gauc;Cr70XQ?#o3$A-n>QmB~d>bram9 zU)|71s@m0`ISp}WM?}L3AhCg;oelPcqsegQvQNApDNaJZ*O$&svaV*zsz&oE(z7kMBg=rIfa?&@RD49A8uyE3B4@m8*Cu zz@^7NTJ1ivq9M;>^KRs46RRe+n=3mOT?vM>8ks5aH|yiE{+*XWG}h)#$LWBPxqK;q zIqSNbL$m!Vo7?@$8_^ST09NJ42BfS{^hiI^%Oz9K`TfIlWC0<+axE2X%Ny@Pb^#Ue`VfrpBl19uKEf{I|N*Mha zK9%4(UVGjhzUfS4XP>yvB|q)V_??d_C2oAL&t9`M-nJ1Jxa9qbM1gl#qPjgPfT=9n z)#~bNXRUx_&vh6vorgrxyGw(u&#zFfVc5(&0^EQ^2Tc9a)Mehdk){;!YCaCeK5vp` zF6TM>!KV4zTpHEgn=(r&GFULFlfzIf3)C9^O7th|eAUgaJ@Hmw(5?Wl^eTUG(XxI| z>QK|c954A9WY$(ibF<3r8**bZe>ERq+Mc)d3KS^#x>^mWG}%oi^4(01=QEzEvUHC3 zdeZYoFd7`%8}h#HYEGSVE3SH?GQK+2xUwdhvW;uWUAqcujGX)Eqhw+u)@WqN$p1pA zo##)14I1L5PO$N2G4aW(au(DvP9Yk!q40^X_wtR9KqMD|lB_1MS_$^2jMuD-LtV5f zc`?B#+PU^z*p~&)<4HxAq@|r-{BObffE_+HyX*ZbAz@s%IR_U1;_#xSBIof6<04jG z%ll>>sLQS#ONGr+J!xc7io}#fmB~yK#-+9RAobC+dO+ed3!hvAJX?MV4W&0PV0_2$ z5p}vs>d)L|W(n9QuBb>$x(fTb)tx)o8!D!T z>iulu7$|LSe-aajVE(5E`ff-dN0|FjnV#$T{Su>bfaB#rp|(S=0hAl(<#$B-Zxd~ZLNpG2NH)3l56b`DoyC9@Tu%hx?w8%h|%4 zm!BjolpP}8s4(r#RUq!t4m&N zj$+bYwWUSZeW6VO&<%V5%Vt*@8RX@+EGxlYwt4wZa~))l>*s_4hs*zUv8Mm65#+8?f^>c4mGUf?Ybhq$D0G!jB~e$}hpZgC!aeG-p&L^v-(j#mPO2z!U@ z?tsbt>>F|JgJD4ZfCurH={_iFdVv3>H9O6ZiBwv}m@-JJ z4XR_litJ*a7+FLkVyA_ZJy0fTHD0sV+(wW_Ox@1ePsrS+(Y;>*h@Xci@KeupKR;Ru zSUsaLihqwVsse8_EJi0F7qa09OCs-sq&Np?Htm{F++|R89;#%RhjxS!+xAA=eipTe% zpUMER*L_*|yAj2$#|vw>JpIVOw5#+73?x4R&#<}2MrJ_;oS;ZYXH#tYQIpCw2L29_p7J^%=zy+%$zPamu-efo1c}Fma2_IkVmj1`t)`?z;H%yU7qCGc^C&sL(gL~=wb7dNhIbn|8Ytk&QTFC?wcbi`5_qf zdOBN(!5tjGa=8)hxp_7XH*MT!Iy%d7|9*5=jyP;)Ze-)(GLqC$DGNL&kKm`$cnq=kv(9!{mlZqF99+x-{^#ZR>)i!$NCV6HB!9a1#`pE8w5V)?4l_g?zo8JV zV9A6hgvX|;wO=KH-f5;HdZ`YrGZS{-CXYAJ} z6;l~V8j&rVNzXEp*ar5KuddYY7q_^9;$4xaPH{87R=aUp;4xo2chisN`CqMtbw~Sh z`Byv&5gbbsBU&t5$2-CVzC1CzTfdh0&6DNHAlIx6=c`?u_mla#)`JfGaaKTz5CWE^ z%K%ji+1Q&yLlYaCX3btgB22Qyw3((0;7k>V@e@(k^Q{4w5t3+AK$qA>=ykhYThK!t znUJ_m9*r;mHtZ_xJxuZY!@j&c&jM1upYzC_o|DK%o4aC3@fyFzImSwl!HWKNm(e&w zc}kJ9q1bhn`zjQ+pl(|n$-`-*caeIa!QoP8&m-PgfwLRmlGwk(|&Y=>-!-v*m2f@6%32mJgj%>e?;JbN; zyaXIhtwMstz7n!Myzf3mOIkY>y0Yx~(VO33&M!^&{td_5e(W#|0Kk!5uD*26@>VRw z10|F0lqqFFB=Sc&PH1e4oKYM$!NO{!vY4h~zP-@WcAQWZ2;?cPW^tn!6(- z6UbA|_9~#jA!*w@Kdh18x--$NqMugAQDh8w%V?ewd`L#byWcw9rm&W{2l}G{XC^E{ zWs94vupd#tNKOo1_zN{X&)-s6GwI+k!bq$`fJ8LYz5T&c<+|AsdhdvNYx7Xh-A}sS zcHY|`?Exe|BZ(KuFGIubE0VLM+1_{Om}Jk#&v(*tD?#!Mbm+#jo06tluGF=zmQaNx zeb_^%4F9C&JI7H{Rs_HIC~I6R>ha&(Ysbv!Hd7wOHmfLMb^EbVJAQNMw#s^N$%{_< zL%fS*us3<}0%R$+zA%V{!(S!ZQggu(AgH-d$_wHD?`!WU2`u*ydJ`7X>B~xQaG&y( zR4igj=1S`&I%CnGp2>D1)t=XD)QP|EX_B7Fnk4&(UF|72xbH1OCuG>RlllPWcX^j% zPq0>pHdN3D;kQjFQKlR@$lQX~z=W>%mo3VCntq-FvvOS^2S4^Fbmqx#Esw7hv44}? zpBrt4iyvVu7m)9yhecq%#ylr=OG{a*vY)yZ-CX?ssS-puhf2P_nVDdsx)Tcjl_Bk0nWzd`;CP|xc-et(BpeGFOX{JImufb zxB3p!cI)4wtxfrQdq>O5U76yAM{M_pqbGA&$lu;UjCjSMr%X|3B-U7j*X$2+(^3mT zq{GpSoGE@wIrw010f+TZXM=oPh-N9t2?;H|0Aga=id?94U;bEhp)Nty2RAdGi$ixG zQ3a=)3&9?he?1JI+nRtyUzS%A8H@6hNe1F#tA@Xx{{a*MNrngK=bPeU)HeWwKiY$S z2P>kLa0uEVU&U1H48?xf6!i`xnjJ!8ZEWz0HYd+B*qQT42b%WlH`@uJQ^d)VMWk4R zAh2CD3h{U+ibMKz=?y^M^W9w<;t&)ksWo6|m=kzMj%xw(@PJ02!EO2JW&YjM!IVLm zyv?RAugDhidg$ydCvL=iL+EY0`=4V`i-+ko3OdY8A?td-&MpgnnvF4WWnHOMF<+0d zqR*n`>reki1yr~+`B&#W8yFuE$rS{tFkzEQUsJ0Zfipwb(;|*_%{1u`cL4c?(&x9C zglvDJQi}xlTIQE7u|>$dW{~lX-=y1XSaZOm1xFOnde7bm)lc!3s=SmNJV=oe* zPkmwWg&^emoUJYT0VP1AbJg|SI|8Iec0=3UZziF99F6Qwlo2&E--|^R?>cn{y?cDN6GHT_y$`7Oqz^%p1pwUh*Y}XsvC9CNvxv94!&EvCRBZ zAbMvInVtrL$to7I5`Gmrrj{sPeO-*$iELJ$z%A-;56!fHuT+Ubn-23M<>besC<4rW z-wTQ{`6$cn@16Yz?`DHK1({q102uBEwfVpEEtRC&Ozo3E#4aiO_ac9-13|7AMG_UQ zrF;^ar2MnE!Jr`AAiM!1PSJmNP%l+tJ_<1`qtrHk3d#S;9R3Q95{Yp*;ndAr(V>QO z|0mJ;>l%1(U}<3J7}ra<|2qf|Cl8~9;n@fJ_lICZB}3ktBIF~4uW5b$=hvZw=917! zMQ^hcQ~rA#o8r7vNB9bpobQx1{@+22zb3`CjyMqdpJk!O{5AhK4w@PN8H|SgYf=#% zre1$9^RLv5N(vV`|B!xrW}bfzAm|_$6?9UAPDB{k|7IebD71`!&0oD$#qz&{NC+<} zaOzrGi<)M!mul~K%Za@v`xAD+8pjEGaby23Dmah=ZW{c%aCCGOd6Z7^dl-l)4TeS20KdJIU~rhoZ>< z;0iy{bx>pb&O^VC@la1@ApYo5XB0~L(_de>Zcfb(3@(A~$9$K`dkh~4qv43L^}$9Y zfDa2-u*EnuqAB~JPb2hTnP#WzaLF%fSt2fhO=9QF(a+AZ>ZagpQc}31fo8F=t$3Br z?rB{joQFiF&V+tK=C8~Vjdrn;TS%4V>B4CZUrmwM{s{TsIWBMvr`Ine%?5@4;ltFz z3cOU!H)L2#P$}5sHy!q{ok|+YOJCD)0mjkqBV9(>JYmwSZ$I`g7M@;xc-iVGcI9B; zIAPdWPYLZePu0)~*$bi7l6vjIlBsWQ-&i2>TySW$C9WD{v3>;w1#XWXt$V?|KO4Uh z9g;Ry`acywvWC>pS{DLnTSQ#n5~O+x3kO z&*R~^EVqkIF@v1Be#%SRE~ZDZfA$ymIAF)gA#VAJv~=Hzx9{&)Xa_kMeuQIo9#g^k zc(|zXYF3Ck=JsN@>h*5=jnwNO;(d*ZvmTz!~%R4!iL zuSnP=@=(cO5i|tQs56(d=^td`{$}3agdG+7xu5Vn@9tzHPrK5PZjL!_N4kkvuqE}E zSJeQW9yhaIAu*j=F1a3?`)G0UuTFhJdr3*M%$8GG_Oa}-5s}hCy{os<6GuYvIL#-E zj;chuj8e1QE@luZ(U)0g^0~TC&l2K%Q)6R_syR9KXKv2U-)v`PW{a{6l)91Sj;3=& zuIJxd+T5LYVgF#^SmIt1%w}PWTNZS47b7Y>82S5_Nvs2}jTvXu^-tG#!Q65JYhbk>Udr%IYiMWG0n@Tk_ zcgDyw87rrgb}pBaiP7@%lgs%eAr?Xox}6)_eg^$cGS;Q}OLH^2bE~|#ZSBx+Iei|{ zfh(^o*pP$6366+4sP?d2GWh9Qzv4iv!vp!tVy$?UVN<4|kx@CcJZXsL%T=_*nBi%|DpRnv2F~uo;TsTkP-@xExPSX`%>Tmn*dH3z36nBtlw+Ubw=( zx1wEl4h)x<P)g!6s1!pJ zPX{&h%PSVeNNhks`9eTZVj|8@gHpnemAb(tufm~FVuifK69~U+?tm{-02X!%m>Pchz;e}VAORxc4A#Z=4 z$@O1svLzbNVIg|%hZq?e9YO2xS_RVH$18#WiGa6I4O$A+13(4p4py*OzQLr!4rrfT zt?jrw|8OQ8hYE+=5B>j88E{Xz)3)@r&!5z60X{a9y6;fCZm1O;n~YRnA3{LP8zyC_ zu>kV(^BujA89WJ3El)CIy0dnV2Y*~@=7RfL{(+4^ZYV!g_N6hI4+~C7 zG&OZ)Y3(-ybAP6eD_-eP{E+{6727fNH6^l4ThDR)I76D*)mc^PSkE+=82qa-u*|*6 zZ!cxc=pf3M6@yJbFu57f{qEdhtju!iDLH2$9?`g<%{F1~W>#de^9!X?9zR{2zDM>` zeRk1|aNEKe2xaamKV&PFB0rR&I#G?^^NT?b$_5H%sa^%vL~9@|XPCbBj7$Jx{cLX0 zp`NLT-1c2zncn_-^48h{y`9_)wy@e!Hg$-uTjsE0DBh96M@}^@3Sww}L!Whbu%O?4`Q{?TI)eJ;}A&;3>iPX}e7M_(LHZ)Nc-Z|5I>5 z|3uT#0xSiSMTzqDlmJwA9ig&VqQa6}X)t%W&vFdq4lS0dao{qFii#dt5=t8CsRt!; zoArOCvBiZk_ky-+DaUW^w4DI2A!<81scg6Xl9Y5!Xr$^sIK3INeSb zF%3&Gj8Q+eyfYkJ#coKOO1_ftsN)h6D%JJEx#A;mqUe46sYUow@a)}Y?p>f!d^v$I zrQFlqb$pS1I3B#^|sV=^CkV~$13IA+~YF)H37zq_uTE1(!a((+J+fN zVx2MCukyM7}8%?o4$O2xL9U* zT9NYBN`!B;%T=Ts+Y%;b)^hUNjALZ-KYOmW*$Q+$d~y9b+ONY-sOmg}lkC{WDe-q) zfTMtL-Xkf0=_O_l4tTJDz@G4W6Y*7LzaIvP!RmOa#_jbmt`a(>oXf5lvge;UD;~)9 zrAgb&hlpvh>ImZoEFutih=d%XGMvnnyn%ea$wwrEV+=t|1{_?J$`e4{v`ge{Zl#@Dt8A_{=IcBG7MH;=;aB@UFLy_EAXZ&R6qzOx?^qvm@%1z^YKloYg zR15;b9C##5roRQ@7)q>V<$|L6!b$5kV;JcWCqU%(%fWR(S_oq2y+(z{}cKW zjQtOFLc$m#4kd4w!@}fB%QE$Cy79@7gGGg6xr( zlt2x|#=N2QOWC7$dBsuI_Psj$KX1xp9-EVc!!u1v)o4BM7FlbJN>gjPnDi-_7H=GP}po__Q+}`GpCSErE`62lH<@^<>eO@Ti?(dSp3@j(vZ}|m&Ih|Ai+E( zO^2}Xg>M{XiAj^*>a~&+?D%D+7178%_;eqwm??Y8;_>!8>B}0AiTzshx2xH#0@nO< zD2$jjCRS-!&T7=|7LC|r$7>42!EFw5RP4bUuNNZy8?wizaoR!%BO1@n(9|6B%P8~R zKW%&jdEZX7DP)L#kL`tVO=hr-7BodX!J^gRu7j^ZsXh)qp&Vx2*2A`wIEnJ3GoYnjK2s*aAdgWMPln2y55xb*be{ zw(ipnH5!cQ@>5Em3(56F%hb=mCTIT%a=Laddg66efwBKhrtIzNGYmpD4l{bm7+YmQ zK(QyPiI}f!Qea|3H^e(1FzpB!?t$*7ZTa$7XkS1%o9R;%o3l*!!kmoJl5|!*?gsiD zVLFQ`xJgCMu`&p42x8F(yANNkd{|beuoe)}7K>O|lO$##3wpecqO9Eb#Au4CBLTQT?pU zHl>3SgG5mj5-8Go-do+(kIzS+JNwT2mZb0!kh&g>s-`gO{Gf*t%(R!7ZJf-vEy0CV zUzZ8`Fu@?35Q&5bepmp)Ahc(ar0{Yne57WifC+9Nn*AIyRmfF;c4}rgxAnMbNX5W` zh;`?DNfWp+4KdfKa$D-%*a3G-G80MUnpQlY%q)%+e1tGHdH0LY`z4&JVp4YlCGv6W zEGLJ5uvAeJI1N$ik4!m9E)6hqQ7UHdTaX);_9ziepj;3;JoFydG}W9QMMXWF{?>`y zVg$CC%JU&c6=og%dT@Hx&Jn8V);CvPm-B!x`fPxYY`NhMbw#dU1at~u?eXB0bvz+Y zxAtqDA%cQ3GDnJia0|x`%Q$kuhc%j7-+w&0Tnzfj#?;{Q3RP!c@L#3SG$3OJRIp}s zMLi^}v07G26t5_!jPz6(DZwZ zB!VUmFF(qxD3l1QFfH)tSWG+Sq9NIfN-Ay#!}em!=l?pOOA7vl%>{?ZElm*#6HHHr zPP-LwC#I5n$>PsbWrn1`YpJrW@NOVv*X;(dwF_9pCzG`rP|B$7jRqCNa zD&+E}K8ge_K($mg57!KfG@pd$a~;Q=GMFs$g!!^Zdx7x=Q zY?~NR!}diEYwyZp46~DDesFWS%RX8o92w&6L;|Jrs8_!ap8<8cU23r)Y@| z$Zg50mQ**Rh7ZAgGIG3hKR>l8*Gw|!&3yL4s)gDPPK6}$tDud4V;8btC3*M01z~>Z z&vtSKV*K}Ro8z^umbp8gmIb`mg}uq8uhK$d)_av*ZfEq`=4;xXA6ZgD4;-+5Y`v^1 z?50r`w-fkV8n=y=ux9n*$+hUBgg|Kt;{&{bXSnSy&-^JhqGZs1oCPQ)EmReTWxNS3hN+Kq>>s=&FKM}(~@UpPe(;C+b%t35?KpQ z*V~}qPyxmfvjzL;+tv9?A&HTZBDR4JQB;TzIC~%JO9peoi9lh+$DlYVp?fe@f{J9Y z8yQol)pRyp`@)KLLLap#^xyNwd%=>BD~?^8UHL##Fa(G2lLKyc`F0-un>Vz6eRrs5 z(*DN_R;KnkE_03HhUmCv7+8lO*bu;n2n>=zWK?Oq`^eC{u_XRU*E$P6kl~cR$U64o z*~n+Mz*jb{REokm`gQb0WXeP=Hp-{mdG2ZPP4UY_-HX6=-vlPJzk!<=m`Um7>Tq1+ zea#Z5;!2VY$bPo$6@G90#CdtrmEp-G)>QRrFFE^&K_#p-2-rA5ZMM^y9gT=iku_t~ zE;RyQ^TC5(uMIrTuErl-*C{=sYJ}GgS$Tt$^oCbj!HYmQ6{9@dn47S0{X_dKT}gED zty3kEk#&K(XGA0a6lSAPR3r##n?>kKhe}6&n+y*LfeK-N#KZEjNm$xocX!wDa4{d) z&}cJ(MToRfZVs7G4q;<16EH@g;yfuc!;bY_6!axcX!nra)o{M$wV1eI%qtR1PJlk{ z#&0@~OSHz`&10_s(z~lWEX9@OyE0ok^E_-YHTCk*6|PBL=$#%e-upCmDS(^z1?*IM zs(`}Z*w;HR1oF6z_|_moc|IoXz5($X8lU(bb?7~OSg;e#4Q!Xz7qe@EF(%jzw3@B}lvu(w0 z!L1QnZ~U@NT^3A>D@0kaMlFh=R=W$8U~H(_v|}Z-!+dO(PmhFe0StgY^# z!Lq{<0_$&(@p#=Ew{iWGcCXBmGV1GLtOBYe_?ChSgb70)-a5E;*woX!WTE2f<`CC# zNnp^K()`?(now2Ax9L;8f0|IYFI5^;KCV&AVbRr`TU2=?-0Zrypqeh$LBvjHoo#H8 zpsl~;{ezA#>-%d_~2Q_#-ghf*C^O-)a8uOIok2& z^1qbl}9P&D3OLCz5Kj@;f0u-`z9+^!354Z?K z{A-Zo9+UT!?0+Fs98c)q|3AGbm2q=_)Yq?tzNn3!AJ?<2mHvzJrsG0kLsTI;72TXk zO;pKFt5Q2pfDcPHyK%Ras;Xji5uO>04)(cE7D|R+JS!c)-wVsrU~!J~*y9f)CU*Bh zx{JVXc?|ZhhU0XDVqUoS(U{1#oj2<+|b#!i%VgVKH4Ko~th3Z^`U&WW~;EV_Cs#duUV7tO?M#5uBTY_o0K1;VI*v zuhaiLFQ6i>dfSfT#7MDW|4G(Pf5w>Y|02pzUIYm>6w|drsHFbSu;k8P=y&wpCh_!t zFl8@!D5iWw0&6?^cf9>SK)2UlK=DqOKm^Z!_ChW$6chH5?z-j~Bo{^9i;JgW}V;c&uoX%&;y?=x83UeyWldtA75og4T9tIZU>3Gm#i}*2mOMK&CZ$NUhOTp9A zh~A}x1E%klAv%j^W6d*XUHHx0gqU!^oqnPOS^wg}_@W9l8lI%ymK;ar>BxVbjF&9-gVw%N9AY_@H;*|pi+Y}@?L zb?tTkp6A{7MaOi^%sD4M^q;_yL~^?faeZ;vUA$%&o3dYU*qyFR<5R4lXKmFSg=z277fVzWW zay9)l#v3G}T(&P_uNpR4O`qDi!ec^1b8>}3rjzx&??7nxWUg99ZLonltUMsCT0#-X zcSNUC4&}uG;z~ISVubeup5{(c{P+9+9ptYeC{v7f7oFhD_H*n2`Ut)Tk5D+QWO7~L zAvNff>BA6Be7furJ5ywr>_8hcjO$~@y?TTQ$H^C+EwHt<6$c&9(|hF$!0wL{$8)(X zg_3(@cF4xa6*ssOMzZ=$kP|Fp86EtAgxXS^HwKe9GxDONL_ERx2SY6rv}r?G7^du0 z#b-kbW-G^Slf{EtC-Tpl8(Uw8%m9pm;@PL&)Xj9c0l&15l5$+DM*I9z6}n3X-wE;s zHX-Bz5G9Y+gX%d5|K*1-zrp!h8@b{+kV<{HKQ2EKM|uN(wDKK;Yj=)PLQyhxF6f-m z)TO0f#4bC$)h;AsRhz=#06SrBJTdRQR(YIokVRPZ#{T`)?U?I3g-)A&us7f~0!(^h z06w-Ws?JqAGZZ!FY(CV}xZg4Kipyyq7}wTnj`F8(u#CDvY7`_w^Da%{44Z!T)=WwG zrNQUJ+ulJ6T}hS;gBUkc>uKsHrT2diTnuH(4r9kS28k=Ky1JS0YPdLiw&e!%_t2jr zqeVL=;%&OjF5565`HA?c!ShIQU=N$`U8{MvT*h1>_%`bsG>Jjw)Ds-0+3?K#Zm)5D zKuC0W@X_%+vSyEe_t~H3(gZz5Ds{r_DRwui*d~T3h6VQo0~zIaPq)Jh87;GurZs7L zpgp6M-e@9&N6woxToUgcXnJsi(GiQu%&}Ok9>rB`c6Q>ly=1s)5DKgxp-+x?gF9Pp z^dLA}ug|WYRZIjT3t246kz;(Fbr4!asa30X9P+dy;Q|r6;sy$&O-->f*`3lxl4(VB zb(c-=RuCNb$2dk(`y!}RsN_sJlj9lJ_==$c#X*5i9@Hgwg4?3v;AcH&uKY`0;r>2lw+f`MF}>*I$aU z7pq}80s$ej07C$23sTXGH0Slk_ZC@I6C0RgR8gRjOPOy4Cp!{)XB2eYq}->q5nTYQAy_B+2xtETAWE*5kqna9*w#*zJ(l9GyUs8sOC6q|eyzT^F8lIZw4+bjX2% zf)c-W&#{`%ndWlX@3JK<@Iam|)yDJgwF0h6@wU`t9*+m{w?l3qI=3JiZ=(okhM7rY zu`pO#uXCbyI{2kp-H73SK(8Iz!$B7R-$eMK0KqVAU`?sa&;`R+3?AT$t0&arS*Nrw zoeF_I9uVR1N*u|^2jDevMf4ddODI)(T>=gt+M?*x2|q0|o4=o5UrkKxH|{3zc&N5c zoFmP8rwbj;7ALmX@6%zR;tqQEn@na!N!Hdd8a-RKdIGL0)O8y9!b1P2o0|LK0NG4L zLx;?9D&O#hiU-UzqQ$D&;=Z9!$|~2|UdO{rg=dFpd-oPWjgi=%c<mq2-MFzNPK zbrY|-TrP?mPgo_N+qqpF@^EPvSIgNnQ+Egyk5}qqiX4PLo|ehW@7bC80Oys;hL+I?&bqZ7E{r0`=V$Y z+^;dN50!o@Vm>6g$Ju(xeg6Ijf^XAlxo%JSYLDu6;!kEEa2R2CJY?{?-GXLzyH-9o z{Bn4+)u(>wD&!f=`u@f?U!ss{e=sZ~lg^sk%;i)ttW*@911%V9KkHFcob7`x^^Z(; zrKd;W_G*YAtZ4{vbmH-R{*5H*PNmsE!C|N6?lY6kYFXnHXSXvrYf-7Gkk#&Xh}e~> zT&|k9P`$z5(_`{Tn@{s6XRXw$%~{Z9v%5#JdYe8mmEhlf*(HG}NIWMPGb0YGb)lXK z?Pmfhwfkm)Zkl|5Y5s1?>$~AAANn|~WR#pY$`n3KBa=!PutCjsh_<>((`ejf3aun* z?kAJnS(yuIN=p3>sEuy-?=xLB;TlUYQYTsNVWa@CqDDmGzqeVQ(Ee~7@n#b!ssS7pG1-+q92hJxRK;N1u4rjK3<5Gkw^3W3U{OWH6Z^J{P#SU|$U^0)n+#<7xmb$tIEh4oHaSfS%Dcpj*Pjj86J zlfe8uxH9Xy#4;BpEKAdom21=Wlm?kWS#f@or;y);>36W^vY}tf&C~L$6(+U16T2sq ztNgVQ=yI9J;u)Q>FjHkY-=9bhI7`+~==#pMQtQRFW_$-|AQ!823{3WM=Mq`Ol#1UZ z@i-ey%odAu%Jiz~(m(3t2P6xB1)$|((56X2i^nensKj;VAsnwhc}=5vO%>KJWprp5 zML=_~v#Yy~3?V53@w4h}j)Lp34IQ=mWk>%Vhu@`O_%~hO8ja^#Rg|8$iX|GBKM2a? zt0Fyri0E~1^(l2~mqLH zu~a4>mfK@f1Nxvdbx8kPu!8!AfrMGf235dPJX0P<%uT9bUe8z6GmS z?K1TSFngLi^G4@zo=Sr1=3bdE?xR0j?Q^@?c^69PptLfmaoTYnPxijcV{al?IY1qY zWWxOJvid`(+18VNeQ>wO0gyy2>6@V{_Q*-d@)#@h5HM!IpCV*Eobic&SpJcy;!akr zR`jE*U4R2a8?%S0N$S3Ubt5Iofw+~1AY>LSG%)oign0XAomaBH(rh6#5RRTW2*wV% zzF;rbrJ&4Ug{awj0@7$P7tCv%``v8n)ALS0t5GzV=ECE{MB)HcVU2{>tfJq$fh!x7}hJ< z{V^QxOhq)AFX0vDQXG?yyY0FB@jx4YME6*Mg5w{+N$(|ir);l8p z8aoH6VO|v#fMn5_g7Gs3xYxt<bnGw_MNV4~kG)Y#?JGa%(cY5ZOJLk5%E~i%Jd| z8H~VMt2aME4F*gvQg<&x!-Zk=k}s6$$NZY|Twi=A_6;dsI85O?jVVOnMxKI4lfu#( z%rpq-;}C)-pJndl*_IaRN)~uKgbv=0DvcO-?cDJQqQw?tCaMuK@z;xKm(x-3tpgY? z?iU+Pxr_E2{Bqvvu*-lDNr3n6RWHhp^9mfiferjJSGHF!2;NXbgX)IjiwQxTAcaS3 z3kkWb93ie(Yfoym((px&oW*P=Wj?b9*olH_b{O*j3`#6PQn^1R1m$1RZnw`DP&^;T zBDohUUYQRs)+qh6NrZ(FWr~j+g2~kSgQW-ZC>kqh zoN-hR5JX+ycZbO8r;^YDdDcUq+Sh2RnLPN)9pwFL1Phq~PaJdjw6D5!h9!dmk)k<2 zGVL!ix@$9-LCSbGIO8<$>St_vNLxdKI3E%XF;&3<*I%JlEK2&EW~bh62#J8ln09~8 zMd4r`Rfc{vWH)R2?%&lP^9h?YvpGe(7t;7W71~-|8gJnyMI4&^m60lpr|u&brtqct zJd_jDo4RaO&mm8Ld@%Bje_GKg#UUh`=eN!fXkWN{I`kF-s|I)Z;8{%;;7rpJzk!mV3kOt)go`1 zcfg<`RQpw(tHfk{v;5pqH-DH3g8UqgO;h|MI<=~3Ex40%SX^N~uR$vef%k1~0j+L3 ziTQTFggvjveMU)?k4@Z75FDmR?zwSwL#SjXs|6Yyzhd-!tUdGp5%2>B2I6yB9WVKa z_?({y1Oxf~?KBSjmmM%gYx``Zxn*OcZFly^DHh6px4gydZzXCdB-80cOAKO-Q8(l* z*-#w!21PJlVR9g)Z{*2laH9t~I9DVwJI6)?{*~B%BL7BG1xwEGZ5739=qwEjbSJ-R zlm^`eO_5qR%5XJLky<1EnM>5MSL^jU+T*1wR7ST(_?1Zar$3?!C77PkSWf{7JU;LK zbNPYlB@uv@Am)3>y&2BWm`e0G1ZGzjb@(K}L=oF$>P0S>6}{4L#p`z55SO^~DH{5s z8x$*-4uX~(Q|>AMAsMeF-*`d;ZD3+uL4TV4iX1*KEA`VDRc{Y&e^eX=3y;PuUZz2? z7>t3L*YJhIa7Z%o{ZftpOnp;-fSUiBzWcxuwCQl_tTK_7tmIG{nyKF@sRK?Tv`=D) zfmn=$ATU4g!r=c@1!A#}*U}C5R8{Y1-3p(|acySpTI&OnLrhd9hs!!xtGi?Y!Cd0J zuGjOu!NIxrRB2tpMZaAO1@#L&i7Q z`D$3E*32q3f!{ls;|IRHpY9KY6pL}R-%L$*MWUKmEU5w+d1x$_OZ?Z3R)4ZsEA=J_ zHtM`q;nGO{C)NiMvQO|#K`w7(CPZ`yn!pr^K--0+K4R9078m06Ou_Jp_TN{){Io0Z zC9=oXxe2O2AMwQ<8R-K{hbqP*W%|+t!L}(_;*-EhP=F(4*URXw0*0B!vI@8lHZ%TW z9c-jpJWN_{$nr?c(vP#T z@iNwrvpMbEXd!$rAn&MDB>Kk63*sf51m*aUl#$73qBZ=CKQ8!~i65C&WKzQ?>L;bY zK@A6t8vvo4GskgzCFT>_VBXr@0eu+P`SqIm{J2ulz2p-=iv%Gjeg<0<^dQ0Tav<{p zL15;xv@z%qkN7iX^A;u6nJe4i%C$d&uzFyO2&ZzCdW+!vJ~`eY9T5=`<*^2~jA3tn z53$#5W*N`;A0UkYdh!^J6OiFeFio^ZM%~%e~7=twjvGFy`jo!z2Dr79G00o0ArN1>Galj5|#?cU;{D?&!1) z?1zSfGGCY<8-+4j&Y|^A1J{KEskpH55Yr$$5?|4F6HY0;Z;wf-GQP3UQk{|$2qhr) z32#lWrzVHAwXJYZ?Z6U3pd;KHsccR=@(^by^+g58Aip!4rX_{Us=Xj)n%-A@nTzQn zQZ9-dzXwP>DBO&zkeof+s!T~p+%t7?A=~=)L{nX@@F$l4)V(77Lt20zn+W!330tdk zX*)66tM$#v^65u}i}JhO4*#rzZj_i-ZGVUPUbv>$*mP$Gup?}UY1Y-6Y?P%n;aNGT zFCv|D%bN$G1c{x=GE8kt@|j~~?did`;eqBkVVOX=H8xMfuPw6}#qfDQR8~|~c@A@^ zFOm>=ubrup6T)Xz`S$kW1VBV~|3||>`GpYqx`|dIr1acsi6K$n5EpgS0C1325CM!} zEhGx!_Zm6nP9o<{DX7E zgKr+_O8pPg-uyu6Ue#}8ef5d{5FPWvktepPh7}CU7>8a``U$UC0_D`bVDbO?K>_*m z7K*DrL{4A`*}FNjpd8>-r})2%5%MCt77++WB0(V_{Qq!pc(AJ|-)WEmw!Gl1e_`my z7{VMl_9dn`E%<6N9!=1i!BPW|naU@Yb%l(?qbJn|NIzDx%t2b zF!<-{HM?2ezbC*zn38u$f24^kltoOn1aV@$cOOBD&oYdEIyT#K!>fe*mh->VR*KfU zZJTbmJC;6<0yq2EHzWV;-!bx`3*j34qeTqtqEw`O;dQ*tRWJ5eh-V?m^p>9l8~1DU zDe}h%_4sZ**^d#Sod%*6d2Ne!+-Y1mosZ8D<)+;gj+F<~8il%*sCH>$vn=D9 z1jQ0s=ieXv3a)9hU&tgRAaVnD$mt%%b%AYfg5oc-{04k$d&I*$l%0AL0)SmQWn^Xf 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
    ^E-&}I|}zZ=J-2g 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

    ~s+2Q#Xxt2eH{MTj+9HE7sDq(!f3xI`zi+ zTtwZ+wPw;_VgXM#X1nG_#AGG-#C{@GSTw0Vn^;HnnTH!Ev)of(i!lY5iFym4dul>K zp28xV6Rb&nZ7GP7+wgQFHa4v|s?SU(x7C*`l{c+0MX&%wo5jpL{`Ma-n-bdctk7lE1F>O_99(EGvip5jdi< zl(Z-uhs88un{$zxsOj5qkZ zY`%2O6lUwvOl6_Ghy?OQu;GZ;Z?@I)`ImbRSEsSjDF3e^uf68KuP}?I2|?WgJyK9{LuP z#ni9K4ts<@6J@1YxM|xqZ&I0&7qLw05txXyY=xQTBTUpc8MxDPOV65A-X^%!t&DVGTr5d>{GhsI#e?4QGKbW!f<*C z-V?j|{eX>u@KXwbfu!*TCrYNjF^glRH*ca9ovX~<1;Kt`&iaxS06-D*FCw#8PSp_` z2AlM$cpM)vK=sQ~uO=BjLrtf4k=g1yXL&p>x<_UT*+JMs*er}hBm}P=tdJkOf4gVz zIXn8hxTWOoL)tb&*cELx6Y{hbb^NE&ii1N|EBGu5F2qan(txcEQ6}YShBat&J9M)N z@U%0eecJeRaU2~nNX%4UtPku!Ez=9l7C%1QBEvv`ebH-@n6}I>*o}4K8XZlSWep34pOnv`t_@hyjXCzO$T+sjG`ud7L zQyJxZn0NQiDr5T|KkR}$^~y-w))k_ zdH6nJ_zmTTFoVOhj}cmzO*hv;^tK0>8$Z?ZRb)bLcyJnekQ#-g$B1GOQ$D?t2ptYF ztK018JzSwA!3~PxmM{4?M&$Z8% z8P5}jXcSp|IJ!w@saTsU&b*WEHi{NFl#*JjL1{nM9>>4a~ zXA!`DPWLVq;H5LI&WTn?=tt9`>AG`r?HZea%3{yzf_0F8-J69sV1iQII6&H_a~V#; zKP4o@HOS9a9suzFbP?P>y|b)3egqLBQ}aS3WrO2H)!Sp|_ZT&KuaO_2A2!P%gYMD? zZPce?fLMd)#@*he^f35O`hx27=Xb4mq0%Ya)CmhEUn}>9I~Ys`-F^GFi+%g!ph3wO z4{G&UONB^9!!yXadoxBCt2N$y!NfTBsr&q7C`qzvFjn_El1-Iw;6t?7Qh~gs>HE?O zGq1_egJ-YV)IptLx|V1iWo7}zZ_sV+p-A68t}KWyDb02wT96(7LeXTVAO4Af9vuL& z)y~3AhnzM2nCVn6fZ4<3&|KS^FB~q0gBBUyqmP+5s2NdHry?A8$o1tH1LNkLOrzXf z50fCjD|TwLIBm}(IGxv?ThHO-UN3>2wFWLAltGHpYj%z;g_|+d+u5YACKxj^?=ZU8 z=vtkdwQx2)wA7KdYdqzg@mCT7JhE>gLo>|>=>PBuH0kMWe=6wIL*dvGX)Os* zsi}eDMZl3u5VmuFlp=2ykqmFM1@%Z{rm9kNw6kw^!3$w%FhQ<7x%?>iS@7#pfTyQb z;%W+ClsDx{E!s3l-Dg^C}LJt1GOqWt1hjPg~x4AJty^wy#c}4jx#QcCtNH zQ(;=SBf-kR!?Aq6G282G>z_t@19U`PZ3_(ZSFCj#l&yz1SP##%-rp-TbPEipFo@I` z?lea`NMfR!PbVd(n{WLFhE3$MVc)O!5?w<-_m{qOjb)kp!amvo^u+n~C1tWLz}5xfQ@(<|7kVMEk6?rE-VMT| zsL6Gi2BeN4V)J^Je8II!b?fAlY{0{|-743zu~`~8o>$GuWYKxXr|Dz_j%A9CzxD?I*f1wOMqV`cukx=PsMg9YGH2X8}@X zkvH|T=d`g>ymw=iTFxbO!n7%s$y*BiZva(J^qiBq8xu$oMvW*wpV5ZwJ~Eeo)cT$f zEklw^S>C&(j!4J|alB*bD<{ll`?V=TAK|lSl$-eJm)_Nlp9H|Typq|fea`1Libs0h z=B9tl%4e81+OXh4PZJ29hgj!q@=QzWPc^BG8sMJK`mWb1wg$knduB+5Zeea~Cc)Oz4YO86H~M^p!IW(@=_f`4z$NQ4y`4Vu^(gL z=X;uL&y&mKZGfLm@qes!pMis#4D!K!tMfbIIyNa`v1#su(a%p%obRw~@hJqJaHn*h zKO%XBXmZa1gx#lW9tO;6MOG!_muf+Hp=JY#la*;DUbU{)BSzbV5u!}qm&XrhA*Jp1 z+k{B8YS?M2a6_;Nu6;a^yt;_C+|j=LH25Yx0o|ql;ozem0D5;yKWq)8N8mXPY_Eo(DvIEgXL87;|U?@7y=UXY+%%!?8opy4u zZT=BUYIm-7*yKIqmR+|F-fthmDt{$OS)c2RaJ3Z}l1tKN-2EN^fTK$FD7>g8i}Z6D&mn zU!TBRzoj-AaE~XmI@J_k+sG)#VVY-YOZQiUBaphM;R-o(cpcAEZtfe*c*gf^LT+>vj&n0~o;hdORKSBr31bP0REGwS{ZRK4)3MtiXU zc-4W)^2jdLaE;{$sIRSRh#6+FK60s!=m>d}YDKM(`l(Y#vqQtyz@esTx-`hbC?M|Z zMb_Mj+)_Y@NJZ}c^!^EbafK{Q1o{?(Z+?w3y0TWP7)w};hwK}d?e=S=9n0VP3@j3m zoG-bNf*peLh79Uy6`1-G&mTv}E-_gu(}P!eXK)`g4e(ymb7@@D8eW&d`cI7zg{lTZ z_UKI9g5pLArn%E3yf;|xFrqQ=`#XKcxrO&xU^h-$-3ct%g$hzQNXPmL zvq-ndUW^QwGhQ* zAzL$kZ2e^w|PbT4mlQdshb|`5(_Zfl|_FKy? z&$QUBjXD!~(sT{kfQEG!Z@FZqwVyYmP6hMT=tO?KV1z6wDwOoR<3uAFItO&Q-@08l z(Hvkt7)9sX9b0*DXAwZm!ENL|&RXk%S=G^G2Wq*57c;GNH`|rcpLuRiFmio1=yYzr zIzQ|iiy+qNIfuFnW_^J=08&1jr)*ISNrCZQ;#i-_g|8wf3N?E~0!|VqTEE%CY!AJ* zR_&@TP}QcWz%zCX*SVNJ!nasU!)t(>H3#(FeW!bZE~AXPyruC#Tg30Mbe}nM-K6t? zG}lvH^A~{2?NeQdQA`1QBpxTRbuF~9kAD}8Y_;Fl&m$U>XEs*_lu^fAMe3D-q0b1e zIwzIZ2Q~!dVozKKy@eZIl(&AP^qFRJ-4P|>mJH4t^#zI;;B8ntNn4*&kL5utwWVOT ze_+(ykEAiaTm@rRsW+7dnnz=L+MHPxtLFI$(HK&i-Xq0q&Pz%0db-ux<#MdlZx7QZB>$V@bz# z+DN|d+1uZ^-oTtz5#vr!qAgnQH*T1qGzUWfIpbQVN_o19^k<;$QQx9-4qSYL*K(95 zIGB)=Y^bG)rC~+DkZ$OtT3M=w$|+9 zftWS;hNR=+!e1xd#)p7*dDGQ$QGDEt^$TbB z*9J3`M>4l#5z#|JtF+dON@2i9jr#T+!?;>12E0ZME=mql6$B`P6+<#5{bhB8ptBFw z4|bn)2UCzN@x_J+wlat2ChMzQ96|VB;rvwxa=-J95wxLnHBT5MPaoNp`j9%&6i{AA z`==HKixKnF{kJ6UHqVJnM*|dEhuK$BgI^CiqX&yUHlgOVSY_@v$I&q?L_)TKn1W}q zS??keUtWLARD#Y*ZdA6MOno`ElIN77-Eg(Jyaq8Ew%zpIRbWUHg@r}&JDUMzEtLuG zK4s3(hDTwo-ynfaOt&KU z_tG(|J6vySu}<|zZ~KNdL?)#HR|telei0m7A?m>Nz15QXINbq*#>LQ^)2k~ltsf|^ zEB^eo^9$`f9Ef$bnzjl198o8p7wOCH_CP~W3I`Dg2F(>_@}8xd)kMJtsm0WmtOkqW zV;9tS^{?b42k;{Pv4%nC)OBF4I4jpe;&y(e=0fsm+zF{^=}sZaT+i(l!ylF1*nDC-%b;g0+%Yl6JDb*O&%`JAeBIv$Nl%c!rq_ zr>6NU@-KE7&_^5ZBh|zNRs8ZLS;Rumj};O)ulbYc0!PE zRbaxOG-YKoe9CH0=+4xuuMqQxCSEU4L}qOS({fOk*#qc8tgE3Ho&E4n(kHL8FL}Lw z`85hze0J3WBFbHs2YwsZgV|iHdf9J{s#$a!qz|E=R`@ANGAQsLejeGvc*^c&XveDd z+gX3l@RQXVkx=kf6c7+d-CM|!_W~%KnT9a))h=7(`MlCXDs<&RWnpuKw~9ZC|eN?=mc)f zc~5?Sw-BRGBIpwrah8>1t4FlkSH z=cHk$gvUAFU@u?JhHgJuU_?8|iEcQ0-UoBm&e)GYVAFH8_q`Hv38#`D7G5S$G^!xr zvb{Z{l;<m1sTIXy(iT~}&hwz0ARpv_)^KYe^cY!<#fu%`}# ztP0$ox`bAXVa>>S)4X%{f^Bak7IbHQFfHaew4t}b)ljxrz!5nwQX(5G%W2uJ1)B6I z_EO@RsaVrB*Cz(T1AV;Y(|0>A0;$iTO=*l|_cfZNQqq0xTMSx;9yH)wu}rvSqRtC9 z(*IV>{s_kp&^HgX`3L=0OVlowkKtR0e8*|h`zWNuLGiz9fo~0oO6Jx;zO!EWb9Z?Z zPTBh7eOmn?HCB!~GV=^#8LA;bI{U#BY#@2KJY zhy#w;s2zbsq4O9Py{hT)!J(AdfpONGjS19#hnq*)hmQc9X_z9Qzdt~7aE*nNeXiOj z_QKh0uqySdG?3Jf2f`*EH#hTMMo~v9I#Y(LbZh z0Vd~zkOcK(x$FLk4wZ1`3l0p#^f`)2-vS?)12BC>orLssV>7a+-Y|>6;MPTYXsJ}# zHm8+|UjKE>!_aT5sC-maNHc!OlcsTl6K_|n_%nFq8&kmQW;<6D68o0B7PbVqTcv!v zcZcb1l`vF9&R56ND;uzoC`?#14M#hoiDcXP6&>JY;4PS4mg`@EDqukkV61%}peG5c zv}6CuW%|}Br30}rjT#x=ye!m=F48WIQ!j)_r-`ulPub4x4h|KfwAYdKS%?IHBCe^R z-?~HA$-TLzc`M}{Iyn6Th~L?{0^odZzjpw#2dR}dmzZI^oZTlj^Q~Y(RgylG(w1ij z!T>cw&dl~*#U`hbvd@Scpl z{0}yLP+eiNDQ;D7JVGkrdfg-s0Ac^jjKoh2Rmw1w0aNZF5krk1&Hxb*D@K`5P z1gD=Uq`zAssi;lK9o#UV{}JS|zEx-Rvl7q;*ziqU%#G*u>$}I+@aCBqy#+*Qt<9IF znvRHZfQPi4S!Q%spC0fL#Q`Rd{vaA?y7Oo@9q%i|H$B6C&NCD<)UwRa&|`PBd#uIK z-aHyOi=6ICeZ9~dCB*V!%QXQ62hJmC1+6S(WY69=U3fdGUS9!Yy+sE`AX0a|Ph*(f zHMP>#`dsl^Kr8cg98ZsrJkBe+rmBb%4{{f61N=*LMWcDNmd50`dV`fjar&QpNb3ev zMWUtdn)ip^W?ou>YRLz9tVQqz;_WE;H2fI~wlmd@56-RAlKyHf@Rwiu^Xk4K5J2n~ zM>Xcb#%q6Y1Eu8bGX7YNjGa7}t-v@YQW>r()#{^-!8k9zg-w1%49)_W;hgG*!FLyf z3*f6ge%9wm0kehD6ESz;s{sQJrz;GNI?F%yWCLo3W`AQhkhMl}xc`VqyAY!s<-Q0%VppS*? zx<67%`;;W3XX2MS`)YcfVUkWBVl;=6Jt{O_-)t{xqV+z-6BIB=Mm(iEAHT8tc2ggt zwp?ae8U?MxtL#=%+FP5v*Mf6o`X6W60&=sAOC)^UP%45nFXRI;%0u6Vb77OJPXFp3 z+nG9QxC^+$$j;Q3(p3fZG<}8vP?ta_8T84T@hmRL_6M)L^_}Ad>WgtrD|9h1Fubc8w4G@Wc zP)&h`J1-(j6KFupH++bWNVflN7c$?%`%eUu)8y zK?tn+AKSv%{0%(c8(6^l0L3{9K=J$)2QyS%zA^!*5$Y1D>wi91i@0A6<`Hi7r?1P& zb99Sey&pPqdP}==zfzU^D!BMdX?`afDZ>iP;q>=dZU{y!eUu6-OVvZpktiB{RlGBj z@sar4p-sGn{>%2Up>ug^1ZvM|e^IPHC;g_i4)22P%&4Tvy0i4hZDIFkF?RgZ6oFpEs^s7V{l&uAFpc{G z`J!Stph$UFmlnbZ82Y%-LoU)6m@S+W3ItBtq8n0SWNZPyh4E@>d-%nn>#NR9nk}H|FRsj!(lW1Wm8L%1r5P z`h;y8A;wvCd!rSeBZW*U>Pm+On{Cb+4!c+LfkIj{r3a_dSn^m8pV0~gfx(-}y8@pn z3TS&=S#C@M9`bFVY$mOSEvs%)1e&Sb}FHZb`Z51hrP| zffb@&NL&%3;Cnt|9JXn;whKEz0ZwJyBr1$AH!#^)3aftyQb-e#*qQ+pWAr+E)xw)h z+B^A#TDJEWLJHXtHU=Qn{8Oh4HfR1QLSi6%BcWNn?m?AAa_vb z3^V0;cR@Xo$9j2zCv#0A2!CL(_Q~P1EY*9gsMkwq@xvmzrtnD+?Hcs zZ4f}@bBMy9TVrr$KYl)Mlxi>p^wH==WtEFvmg&N9K*;_d9q(2vLgTd&0YKLdfi}M; zi(@+e*@@riV0T0uWoTh;83a_|tCANEzf^&Rka?ve@&JL3N`>hF*@-j#By$wfT!Bmd zJKic*Txqiwt9GRsZjoZSD}~k+a&MvOay3(pSSB|#p=gl?)SvidM;e_WcyhxSUEhh< zvbC$m7N(mvM|PGiW(a4bOr{t%elg~JTPR`l61>^scVYh_@ZlC6=yZqrqH*doEpAb- zvB(NArNl?$MgypMnYc)v0o$i-n*VA>9Ip3~J0{&!4;x;J9Q=(i{-^Yu>hm9MYJ9M^ zL;02*|F4XkEBd7h|Nq18yKu zSxDf+-h4p^92Y<+%le_$O8aox0=t$s%4Vj5?h^|+!E16qp0v_fAZ^*_eh;iih z_!b5{U%BH((SKNrzdkw=9axoYG|s%gZtx$j{HE_eA|W7mANu#k^mp50qyoAoKcI5{ zQ_%Gv?hKgF|Mdq?kaXqzvlqZ$i}(N7OZ@)_|GNeLwHkk1{eOG^-dK zSvWoNJ!-G}XGjn@+oRs68#V7ll~?yBQFBl`afSa6``mWYn1#S-Oe|DC!8@=x?CJvTBAbqAuB*97F&br>zH$Asgfr8LUUh@|M9r{*UwyR zC#3q=8Q?x#Z~UK})So_!jRbf=wx7C#0sjGr_-~7HSOOe`;}X=lgyf%n-k-Por$_X| zkpg@d&bo$k#{abF|N1+3hqqFy*pF*rVU+Ja8VFdWI=Jp0meOphgHt~LrF9jOa= zo!42T)|fT4H#Vp8!}J_I;=T>lI!y}fpfa(9uX{0^-i?YAP?QR~Z}Yx6HfOO<)f+45 zS|Ok;6;-{c%*nA@1FtQB`gI33E({sHOKA>X`&@l>**(p4l`f|a$ad@y`0?!diT7=s zX?}hFPDMXP7APA&@;x0w9}3aAGhE!kui_%p3BM7eEY02 zrauOy8{2Q$VB?xk;5$0q3&B0p7j3L}1~n5R>Nof{>bNO!nhcsfC6_vV-)Fw%Js#bz z;M&Sbir56&m_~_-T8C)_$K-e7%GoK=K;At8WEP~tU^ZKGzud1X(j)l=zP(ux9`F+N#rh(+tS#L zw9>P_`!5Hd1ZpW(iiu)hFPD*~~e1vxA-~Qb1x=R(hzts2P$D5Y6kI>W%gFP#?B59u)m}=ISxe$l) zm7x~osFEmqbn$uEcKf{}l{k6I_Z24J{dZ+ryBPKDx}US;%BL~R`Re4}jb*+pAMox_ z=@9Fn9|=pUU5yf~u@&DM&pF*X6ef~l(Qb~rp03a@DK|b>QOg(KqAEp7JdxtaUKD}M ze*Zuq8_`U&c~S_8;Z=~en!F+}mW2gkV&pcHgoL^c-jyVIbpf_?xo$V*PvrXWR`f;a zeq3JM2vw-VnwKsW?~{OuYAda`90>VTUQOb$P?9kTXjgK#c6GR3%j-K=H|Z2ONPYJX#FpPoB=gNqg5mg2gF#N;(0rVQ^Y>FDxn-!+ zO4Won;GaT2(X^8$zjl=hd89GlH)pH+{XnsRSkl*H|`Fiph+0GhFqj0P? zqwRA(dpC8KKG!2Dt=3mW^-j$S`p?`Xwsl;!q&c2GBz^VctcNHM`y=MpZyeNBj>uy^ zd5cdaZ%7D2`3L}(>)(F;Xm=iC`~bx(6@RVJyN(CiV&Y|+Fg zYc|@kX%s0zAoa;#UAcQ-jf5bkes|VN3XZQp@}Z{#vQ|RUFX4Y@oD^#>(T}5@H0QH` z=8*ehme(g%aev@x-v9M9vn5PLSa;u`{o9`}QfTep(hm-GtnKg9oP}L7YnG0qF7X+| z9IPq1q+H@5$i*d^29F&)VV|qC`>q5@j~Qg8pP|$I+G@E;?PVj({hd1yU)m>xLsRvH z$Lh&It*LOr&Dub`3#yy7fmJ+~Rwh}wNZ8M`$leZlb&ez8cu1MZgCwq}#ZyozcH&Qn zAbCYC8U9RWy}AbIQwFqM&9oYDpRS0H&@450-}#N;EzG^KPW}k39p&*%L{zgBnx4dG zKTx|9t@X)q|Es@;BYWpT^P`@f{#SYpUg||iQaYu$nB~szrqBCxyahjWrht)Mt{!1g zbTabTk2@&CIV;!=fTudmdd7zse2p=`%=4LV7Zy_0ybw;kr+whP_byS;DYQI}W@2 z6)KDFdT7!771s*l;oic?_FhfIU4(8e;xX)|{K~lhy8Pq0Ht&XY8lvs}O73?Ncax+g z|Arly-%zS^97Z#%%f|VdHQX!NEA)4)2dz@vTcI85+2?_=9A6N&@L{h6R?(#0X5YP3 zW(`8j6Yb*gHVA%D_A;V;x*WBN{ON{xLy{3C{`}vXB4DNM&+pu)5#LPqpbj-!YAd4j z&FFZi^oYEys%a_a>tOgL ziFa<_QX+YNv_3Ha*y3TVH8%)OZCN#f<&`88%Sp;(tChC=z`r(beaKMU7r~#tCt#zR zEiDTbtc_YbddiXmlx+`Or8nz8T=_*I=tlm=M$h~sZBWF>;~;tMa!>zx+qsGtDMH?X zwmGxlGoe|v)F>;sL-{&r8z1a1Fo?7<)?;~*{aQu3&0tT`prFJ@W zd8y*Avq{v8)in4P^H1D7DmjdA{IaSX#|@8HEkA!qy#&zoA7>2(x`hS)Xhu6VEnCEn ztWH*AWzW}!64ckL@p-4%&ek*q9QJ={(AG9CSjCFws8F-W29;o}3L7y1%Qn)O7QdZI zVN1okI$5e3cJ=j@-Rg9HMV)-Uv09eZz2fi9uE@m6hx;}hHn0YQgL8i8(=Q@y=F^K) zi-qhj%}-eZ4_n=KhpL&5U+OlzUK>o&U*Jmop+O!WpsYYXMaNtoJ|zW(FMuL_t}M%S zRwL86?IKyY$T`jvI95w|_g{QW>~JF6R^mel-0+$TjqXwK5~w-SdQ;5%*#!Jl6MaSrCRI zo!paL9UkWQ%0sogo2wmgoymqzm-QyR21oykvbT<^a_zdtkBWpyY)Tpd3F!_gX%%Vd zlJ4#n>6VZV>5}e-O?Ni}o0jg5-^Fvz^NjC#zw@4N{KoJPB=+9-eP7pFbImp9x@==o zcW>qu?5Tlm0p%=cV7BP=Uul9hOmWM($ynb)oLYfq|*nuVPU6FF5MHGzlx#RL8@~MfE z?FEsJM^S~*(f2mOJ^q49FU6vncLW~yW7?9U&(^*#qOcm!{}?kI>|4j|Lbbb)8tZJu z`KRUn_N5)h7bY^F!C)#VBF7uTH*WI|Yi36D-xy8B4Q zC5d+6k0COAoPd&uF$Q*w^!NrL-W8KFo~e8$GNrJec028_bI$8oA>Zm^e!2QE>39tL zLd>|DT=$#2am5J|Ywfnj<(gg>eK(%-4OSf1nsYR*kC{7LR{2iqj^FpDUTC+03lll+ zI_%CO!Gk_Q_-Woox>j*m^l)G-)^W|?**UV{a6dP)KIW-v_4Zh;JL?BpUWB~&m|`Nq z_}|4)wzCv}JD-`b+O58-Ek-8)YSnb=WyV62y5z(Vnt0*O@b=kGZ9Duvqc7=V(zI^N z!AMl{0uSBr(f+*LeG_a$a_FrOWY>p@EF&1)-c+roB@fuL{*}e(#~k&zwW(8q9HFN+ z1SR=UM4ZE&8OLs!;ypc_W^2`Iqt6#a7;#L{C`0z4fyM9}KI7O6Zel){Pq*BEX!{Vo zj`sco@yq}A@9ieVAKD^_$pntRkXf6Ru5jALJxRvr;rBtDTvK;dD)-)e)p$JV;iThF zn%##ipOyz3OPRf;eYvnIO{N}{dbdGXdhn&q#7ZX|6TtgeXG4-5cr{k_`;%jqU-nL2 zRgxzbW@h2UeE7~R0`S)Xo93SI2N#l|tyddFK>U{p2+G0nUOkLEhXC4!Ew#Gq? zM;Lc#rf^E8k-#4#Ld@w<>@HB=Ff#{vcfLI0Tc~f*D+S!v-I;EUs!!Iv-ImC~r7F=- z=u`UAG+K%5@CpqZ%8(__%Gwl6f|oPhG32fWE~iJ>L|k?uXL|xa!`tx6yAYc=W=eX{ zxTJO@vt(L%quFiY@r3+*X>+7fzuV9HtKh@3t;&C*)1iZ`1CNmMQ+s7Avhm|8a;@2+&Ar;3MF+LD{!n2dsb0O8k zc>%}0bS&FaNGc|B+tu05%Qk$Y-Cx*ZaBFU+wln6tMG9Jcv7fZziUWsi@nDb*a?&5p zXE)sDGj&m6gvjXMtlft!%vX=SkCQHVYz|70>NW_@F63TnNgduuiPAk^G92;^R?<^5 zSeR>0K4EGnmKqGQg&I$W-;Xt$&Q+S^E>69Uq_RKodC3#U-*9+y^zsl*p z5f0C+O;+Z*RMiT0;||Au&tFQEeXz|dt~%E9;P_B-@HOiD#v=~0QvzPartcQ|-?%|3 zA_=OYGe&wSZa+4TuDOJwqXV)TkyUf7RN?P`TC#l`*w%IAu7I!m8Q-CpN^N~_RD zbPyO^z?+`nB`BGzx|x)*tj>5w9_LyuMIV6TX`))^MsGe_rh%hCeU4Om?Lt3;MBd-& zu8CGGA$e1EnB*LMLRQYST)il26s7PeO>(+qq1lnjyETp{9}IgQfFgd+;W-a6__9$t zmMJCZCM_0#?&YYzyj|tG5373=v&b(UPZqDZYgbSt$v0)-u+|rY1+(wP@08<%J8({9 zGWe3VFoWuPw6wCUM`5&NGx4S8dI!npDHfOoq(ur{uHLScdD{T5iA?3;XPidl)NILP(gnVw5mA>b|QbHUXXLG*d<--zDfW+je$A_Pquwx-CR?}6;7i-|W z{g}ie{z9>IIIuA#_x&w{VzGH;Xn8|*s@4`ORAX=FGV6ZH`Y1);E6j5P2mX+P1P$m! zq|w(G(G9dkjw!X?_;_-^YKM#&G7OKDY`!iFfU>QJjHqBMjC zEXCc&QqcPPdo{1wa1;8N{6MqNh9mf@f~zgnwEhF%|C2Com$epc1Blw>+ys4%5udb0 zUXyZN&I?&VEw#$6W1>{-w5dX58lN7p_fM56EOVXLJM70QfJ-&GSmmsmvGwOJm91?? zthC-=c%qW^jJ(0Ri0uh4+Jcsk^1IIv9YOoWyWOeERf6$#d)3{!WK#uP7@ZrmyHtJ? zz&4vrP$kHJgvqvjUkyrAXa9T@rz?Jf4L|-4wXGn{V%#-`&)5hid6BSe+LtsPH{&>r z>%6CY!D6BJMY(N$&_s)eQTsh^YkGv>Z5>yD>&3;MfYUp7gs=!PwPR*q@F9y+B=VWk zEeiA7wcqszHX>TTLzMD;whBojG3jk^**&snuhCNaZ7rloLjr2M49#2dEloH-loKj} z<~SE*X_WxXF13b<*$W+PkUh4@1LL&&KlQd_mM55HPa zyjVjaf`++DNT}JT6`=l!G~_2bi2*Y)2()-+g8zFd{r2S!DD457`wq~YZHVhG`k+RQ zh;CCNgJUpuk2JuKQI}C<>W`aYy@89_W)nN6@tmHyVrD!mO|{`w`yX*(+m|!S^-eP; z8o1hLF_3v^K=IbZw-kCdj@ij6!ky>Zge*eHo-L-@#dsaT*e}E4_uk9-LL~7g$sPDC zzR%k2kO$9<^?=Idgwg?uIPrO}KNxx#bw7MA?-{)k?+f9(dY&Ur*8cG`F(l@$E{wx` zfwD`+7qY;ml<;UF9;S)S`cyPT&#_2B8_u5|c0i%{Uf&Fr;`4+iEpk2+)NQcR+Noa3 z=U%1q%uyv%Q$Sc;itNm!HLn`6G|jg@;%TqH%kwAK4%trDE7X-|D!UDFqHt$?0b-t< zl4t~a9(Va0>8a=~uPV@6xCrKoEbdz~*lY)dREXk~LSVX-+c2Nb=ldD{ridV9{VPrR z{HiSUn94g3ywYgZp}|p@p*U?w@ZcF%6Yct){)%tp(`vk{WYscWOqOD zSCHLYVN0g7G}cDNhl@jz3V^$N2RJ;kV{l7A9@%1@-r*C`SnP{s%oCn+T(Ex`BJ=wt z+CR}#+TMToKuGOU=CQ@NSj!e_oD<GEAvMBrU4>%@Kc=&g7QgKB z4Tk)IK)z=shm2YagwYc1&Pr{(xcIX>xzGT9zFDN$4J3Ls2f&;*shD#_&LNS*qY^P{ z#2$Yp%vtEIN1EbuIbl3Shv9PNK?4kjQZR>ldKl)te`1l;pA*00D6s|WxALuvBTsJVEBkM!REOyG`ws(>58XG&>83O!TKqW9cO z7nM-zp_wSzS#-ta!%5@dsy+I->_C`LZn?LUgu-ULK|zN^p46y4guv?wW0@^AP31j9 zHOC(l|2@|2W`rYlu~nP-jC=>G+<-2&7iJkcA1@l0!a=5vMrF`V$IV^uS)+N{AX8vK zv22@30W$T1lgHtkofJrlO|$1cDZo7;593>*3-dp6q*F#%Vc&ai_;x!5{P;+6BL))gNj5JR!#VN1%{P^)H}62Cy?A z)+sq?lqmp(wC{N%+Nb!ldP_$QXFBn0Jq$eHl))j(^t-w(&R>m9&KAcWM%ZFby{&FZ z>+S%$R_p5Gx{6OX*`gUQzo---_)*}dsEGe$!p6{90e7U@&{Yi$OAuSkZinM-(a20? zQK7`KSlTo4>A62#iIy_!)Tgd;=WI88W4|9h(p3DU?ZY|v-QbJTRQBM2v(_{6-e?*r z_WT%5>DT89x<5d4oAF^ve?JVO6K9|A99Djsw^_nw$Lz(RQ0QjpqJdaw;)fRunh~y~ zFlH!2W5PwGIe=cvpQg64k=`!{e|n8XChl7HUa<50-0)x8<9n%#AQ3<2WfvGk!6 zJcW<*dG=F#vI-4)Ao&c%PYRS2US0b&qL!YMO1ZmKym&0&D*YjU1GnCkwxm|}ANSx( zbQ?gMBa)uAz7COD%w4U%qE&J;Y+)w3vxRw)mN7+&hY<#r1y(m|NdD{TG}{b_sWxi| zH({Y7M845yflZKch=vnOc79o``OWydGm+p@4mSkVSuH?)Cta+PB3-XIUKu|H7NKo8B(y0{60n`D zx>47~?UnuZu#RRhj6&wdsmOP9*yjyz_ojB^Ym;&6n3V!w6Ov>0yamUmXIZihMvoqN zp}JSXDaG1NG29Qcxd6FAaa@E*UZmbA)h6c7IEPTpBqT+{-*BmZB!Zj9kH&vlgXk{1 z!fGCfs&?wyLzjLaf(r}Xc)_UH$S@&;zvTtzd;Obr!x*$NT+XpxN6K}EkIFuVyrJ~F zAYR{JHWiBQe9S8af@cu{pZdI`?9csrPf^6oJu)ne9JV}pnOM{T{e^#RIhUTJ%9E~` zU7N5NXnkn;BA;pirp0q@9}z#af1tzB5#=AAY}!~BzUS_|dx6B*vMrBm@WP-j90ByQ z@SPMLjS)zd_D034$w=h9$yz9d!#}Ovd*k_n+Y%I%j`onq7w~WZJ0bc|d&CLbTknN2 zLFG|Vky)^7bP!qX)3&E9N1u`Ma@$?h{FGZ9X0K-7zy5&BY}tyTy~_78XM47kI}1n5 zvcN8SWkx!cs-S=Q$-v8gOZae-UyGA9O-?iTvkmtCgu-Bpuc!;+!sd1THKr%UB zRIjJ1-1-1X{%7Cqmmy4;Q2zg&Z1@Aw$Kx@&n9ne`TqtNxOYuU)a^c-^6rU3X;goX{70Qc+CEf-ggCmgpA8XL;&yKw+}jZ$)T$M^sC<`& zup&Nx^y2WS0x>o%B=V}e%3u!_jHUN)*+b%A+_fe7+AS@~s+i%3<lze)QXR+Jq;bw^eXk3dVs`L( zE~cH#Xp3?4t=inAK1H+ywDM&-5}E&+jafWctZ+`8XJ53uFi*hzo*@XJ{in~`m zWK81n7Q%*8k^khrkb|mUdB6Usd7qj{x`&I3bch34FL5 zrmtubzK`h8m)bk|9I7oiUX?LNhDIhGtq)3t;*Uo-EAfL*RMSj#Y-+J{X>N{V9~XMv zr&L6Rnw3TY%GsUTU^WNna}{&Ld3jMZMN7l-L;cO%_jVhDeB5C?P{h4WBn0l#D71;V zY<%by3UNX4+!l&6KL?Ij31mF}qMf%A1ZpK;p0hM^s^4Sri=FF1%~ku^fG6BE?+2@1 zb$z?Rb3Y7=v50vbdZL1CCymGZjV5HtU3zHZnmWYkcpas3cRcp6n=R9m6QQj%`H}o~ zhB-)wCPoMrb9JImdLw_67beb^$+{1h9G<+x&E-bfnVW&#^gas?2iGwI=)ltiHzBxF zP&maXbEjRRAoGMW>`UNCBW7gbE5sJR1eiL$%x zEA~7YFJ4v0*eU?JoD;9d@dQR9bg9;^1{O=zb6tpG?uH=zSJ%Zl>~p^p7}V;YU_sgZ z6?jp%vAR>c+*6)VY>nEO8K}aHI~oIp znDhR>){ox<)^5|lJ^izvz0-7t+Zt8E=?gkLn|>l3I~zU8p`*ddCuk&m*}q)TM)SrD z?eY2QD@i#x>JuJgiaD_~Z{fX^N>cumDZ}4;Agfvj*zVmImN_;%w(Rkt947rX7F|&E zGs0G};t;&io+2}&-AU72{*P~3eRcc00*I4~H0!0deZ-Ll$e(iBDB;485ZtXf@OXsd zX1Y5qmke?I)|EY&H%He@D@1&vhM)Jl^=pk^^c%qImVy<>+Xs-9G3b3>`Rru#Cpy)q zhAJbV5s~AR<@V5pd|si@ih2NJ1Jf@D;H{c0_7>`;ug^$Egwc#v;z044QgAMd+xnA@ z^FC?gx>Ol<6V0UUropt-|E0&h0-&fRmt*!siKP3V@nAZO^+1`R%SyI@y|BUA&-h&5 zgDdptOg~eYM<50z`T{wNjF`2%2-f4DtbpJMl)vAcLaWwp{RHwg5ggW*yCH~#DMmXD z69?0Z;G->Z^z&$?Bt-+PRQHl==a*-|-l8XUeR*!4W%wCWg9_xRR! zE`#M{I?sb*z)oU7+i^2>->sq{QNY#a?^uy;{Awaci;XW=_I>JTl2E;*VMK{@QtA(fiqxKBnl2um%~9F!x^lT@zL($S^F_B; z+-?8%d=-gE;4mD~vtrST4m4+Ln9nz4ICe_c4k9q?4!&;pFX$xkDe<~17QlF%_9~3! zt%9+0QQGQG@ zNB6K&3Br1x=enVo^gR%T$85MvBAz8X2b6j5JOJb6Wah{{T_{0Y&FaHkZ5`$DqfZD9 zMpp`})*mZ=o!!%IIMo#k`T}nc)LIN4`P9V(t?@&a0}GGL9l<-ym3rBJaZ1$0uE@RR z9_p;tQo5xK21PT&$*Ou|F*E?B`96;|YH7jaBmN8Yqt5t8^5%eCXx+u3*N_CAn>+{W z?(&+zj%R|0?JeF7E~g({;JKW)LfYaKpVB35jyJvT!1A-QmD}ejXA(l8Zy|M4CWM)Q z0#g42c21$^ZiTx;xf0g>ApL9SvR9~W_ZMS2uib2Oozz-Pb+ieoXthN?Z(Q#(RS|p- z2bKDSZI8S*90{W9($fBi`h7x+wnZMWU9rh(of=HvfxVCoC}O-`ryPxKF*&yt8Ex66 z-^YD(^Qrvql+FG#8Q~gNHBO|~$$OC#0IW1Asm#{^F+#mIo$pgG@YJ0ZkA*y5!1Q7Q zOIRENJ22>$KBIj~0we&k${RbPAFi6ESNvrJFV_Bvs{7+{t2hurEa{rZ1qwekf`?b) z6*GdNr;CPu$nZO53CY^cEnP317qsO`iMqeA)VDHlJe??rsb(uOww*Rt(2X{DS!uGp zcXPfc@PowQ-pt86vCD7>_0&{}oP(@Lv--VMkG4>ax}+H)$n^DWv%JpuOS21L%bgLX zd+}i#75%Pco?dDbJK1yhd%yCki)?=^BUw>0|8N+4CAOaM1c{!7)!_Cc0vbsadxrmc zcCKvtW}&iCiC}~Nkj3eMkk@qSO|J6D<94AZ95*!yT+vUDm&cox#6Nu2ez{s##OH_3 z${GBA+zM9_#xhM^doop7p*zL@YpH$ZknccrU?|hoMbfPW9tOBs+e*@3b%6GynskQh z{jqU;uF!}-T9fCi09wIt3dM4R!>Nw*d`h#kKZ{t+F;Fer9-~X`r1t3ye&FGgNx;2okOd4Xg5-3E~_ zvr<90EVt;sR?~ej9CyBLX+Ut>vRa{$59r<${4imzgEu<{*6((QXeBJE6?K`#_Z;6k zsmkuVbL=P;%cZa&6?1$0`ol?OeZa{YDo|p3jyZkF!B#b0TCZ4}S8ltr2kPI}yiQYl zV@6R#3=TcpUK-w!3X`)+Rr=$d5#yQSZ4}Mt6=Lze?~24KpGh^-UM{SoOXpKjDm4*Q za64`H9QVDLkDmkim57)6pf;bX$$!QhKKK>4HCNzJyL$%3DQACo8ebrv4A5wGzC6?R zk`B~)n&$@^%uU4_&P6rXKPM-bIOvP^C4p(Yd2N!t7 z-zCu=;|tC4H+KvyydV<~dvzyz)^l``vQ`4-{qjabi5!Pg%R5R1Pp?iB?e2F8ci4S> z`He*`ynh7v_S*v1q-C@%q*g=b?D9>6^>Gyzc6%s67ioxhc_}gfm6D5KAkm!kf`lL+ zhgQn|@RjX`c|M4nOL z5Jkf0LFZh;1pU=cCW6DyLU7gu3*A?hPMNrAsHOEFIBZNW417vZl=))0KFCuoPL!zV zM3ms4>f1jaEn))2*Leu@lYeEM76h@k_P3VU*=tYQyxN}rlT_?qFYAT=(9sQ8?ppmb zi0zYm*={iY)_(e?!uTUemh>?{F=ZIiKgtaL#aTn)05pfT8$-$dd7qcZNd%FZI=%W&KZ`rfBhvFATfPx`E&o@E9?JQdWdZgL86TcO#?;QP@zq@i^Y04ABHP38T~rr{>Os{|J~;X zk8$A`GolS{oYk6<6wk5k@>R#9Flsi&3DZvx4hz)HmlvyY{nSw0@+gtMFznUo|WL`M*Jj0dSoJQjTg3-(?;v~R$N6sH7ANu5Pm*@e9*S7`8L0*Ef+_oQeO(X%~n|RbpV$DBeH4y zFkQ&zuN;8^a0=@eHu12X3X6ef!gvnULzule}U*CpCT>l>3&1u4YGt4Yc6(S~4w~kt&Va0Kf%|xc%$AVR^?6;)eG_{d% z*haS{@VR8IhFtu47UT9*c(y`Sj*Oj|a)5XZyTX!BM0c_L^AX)30#1i-*R%HE zP|YqZemAEeV!i~8D72!R@jTC5(E`+VGyYfBaNwHev%;Rh|NABX&$V}e<0bUVaTM&~ z26pvtoq@Qdgu8@_T<_f7eod=E1Dx!U<#&41Bu-$c_0LijvfBI$g$uE0l)uCM=&Z04qHeh(rlWad#2*hm@@pumsNabFiV zaiE>Q(;r_BW_;@>7df97nMc#9$mN&|nd*Zcm{ETUzdMa6#z;DB_iZRr)t{k4+rOpZ zsf6*=-mRdjl@O$;K&u#4aH}nJre&`7`sECx>H72W{7cR%-IC1`*Hl>5sRQ-^{!~uV zO!>3|PqgJ?Z7j}>Wv%dKruCz3yWF>eaa1KhI5c~-A$7|IJPEjZ%%KaH<_1f{hju`~ zP#gOf8NHA>)H3owDg|Lsu!Q0*h%-8}AA}(t5olo;6ReV^%xo; zWBXg1^nV1mzb+u~OR+B`xga|&q(^5s4twMG_(T_~(So?PNC@=zYgqzuv_DW0NZ_AQ ze!S6iqQ_ma%euWj&CZs@=QhVwrEU8PH0xz;b${LR7VO6W-=sC46Ix~EEp%#Vx0kNu z7$2R~+9V}R(8>Xs|0(l8ym`CR;>-uFFn+IqnX`* z*k0DPt{U4uNOx=!Ai^&1q;Z;3j)igWHxK36^sjiGbB*P4elQRvxVlYC#1v-#$n7_>r)zzFE`Y8A0*_aHl6Grudx>X5Y4o7 zHNum?a#OW&wa792do1U7dyl*w7&WCILKn>~FGizM26uUR8SetW^9EDwrs3d4Td8e0 z2taa``3iJhZ>*PJ(W{uak+XR@tyBtK+MJBtr0S>IZ+qI}mVb^Hc5YLPh>vZAUS!uIhznkC80ADhJ5L2d)rx9b!yVFPS3L$ zc(Bx-2bhHD!;A`(N^?zj%tdLpdsKq)UsdMsK0UZLrdxUggSx7YuLwjTP)U;5zO zC_-dJAq{bO1Po&pD32^N^MYMdeIO9bj>zF3W4J|&hd{U%b8>CugVraBjB`LABw>Tlw$#Z?So8uRn$j!vT zsfQylkSOU!xZX|{<#CHJ3caUiQR3c_#ubR+FIM@LM)>rv^}od6g;(y8?d>1lvj>R% zw1(Pp1m|49x{pkla#G7>2C#u%X?WX-un(`Wl zJ}G=_3Q1!z8C|Sf9&=p)^qo?mXo3&h?l4Ig?eA+Fb+RR7Z66h^Qj>l9t@~zAx+zS2 zUx;r|bd0GaE;Bqu09JCOc#x=ZMtcU`$g(_3MD+(4xp?NQ#(cTxlfa+(ZF)1{s8eHT zdyijb@r^waAGYPk$^7H2wE1Q=LzAhh%CwKaN-@OPOs2mW4BW|@8^jBg+*^K#d+C*C zu~mV`W^}+L9A8d^qtcJP93b=*88?9&Q%o62xP}#({HM=T{XTcGuJBZ*5CPXoyC59) z1kkBBohrQ|MH(sP5T-ckj3mO=k!>|>bkWQhJh_0ut z@WpE#$1fhNTYMGbJs7^H2M7={kss8uWIR8&CkmA8gq;R!+&7BQ9YxZ84p(Hq!W|OM z&S3L89Rw617HdK}E?&#OdNAXta_CFr?09_%EFWVcmdA2J@Tm4zB%wX z-R*(oB7N^MzXT1;C4#rp5l`Cf zXKdFg&X|5soQWK4;Fu1f&3AslO*=&~o2wbAX0v*$)rjYR)SX=YL))Wq^u6Cy&009h z70J)qJgSlx#Q&ZJz^V;5l_(r&HIp0Bmg~{@CTU_Ulz@}k@{a5Z^!{zHaKTWIDWDDn z?QNMJIeqW`uS07Q!q=H2zEiW+i*FOSO~b0p+!NJ&mI5HGl;(3dOC3VRPKh)1t_T$G zBd66v-2bWv5HY;|v_GaqyW@MR#U8Ep;$+^w`%4-Si9_R!= z3o!^_pR>Z^wi6S}C@Jm+uI=W{*y z4BP_0!|CUYfv}(~A5i1eYLkwp=2Nw)>M#7+jb}?z(1ZG;G`T9Q;m`L!SnD7lhV#K% zU^DBC>QdKe#P&Q3d%Q4dy$&hT?9#S{ZJwC{IR>xQM_+}MAblPJcKUM(0tAj|av^y~k?hX49v z>G31*DA!Z7GqY2(e~3$K$sXs40~qD#XH}5qO%t@l3{7AX;@ewhmLD^;W|8q)pP*Kp z`Emb9i-@*j?R;lOe8R*{TX{0}bbGWLBCM}cf0k_2GrHz?1!!owvz-Wd)8;y}9P8h5%79zXfSX--rGcnnmpEs<0pbp*RyB=8-Hk87|N`$xMVs2F}S@0JZ4G7 zKz(!a_)rFs+y$O2#E8lVg6GS$9Dy!&;zS$rx%3%p$%jM^%Z#k%q9}!dM7Q_cW%`qe zz<7g$+u(LOs$B?xss<{Z9xjhIJivG|sZ%s#2Z^)MjMAtDH2y4SpP?xqxlZ>dMn-KB zg5};g5Kuow+%eak?`|69@xk}#pbN2bECgm7(PlaoiYeVD93a#zUo7AMW@dOA!L93J z@+;kI58^WiM;Y?D7)Mx4>Q%XsIFW#R-T1hmdPYJHq>|y)aXweNMe034En)Eh zinh;`jLyryr3TOfzp!ez$HB+v>Qy;mh<{#2bOiCxN*Q#;uC}omB#Amt*LPgMUm=rU zgpe5Eg&7a-;GG!Hu#}liDl#ikx{a+~r&bI|RP4X)=)JehpQFe!G$JAzZ57dBLbg-qlU-^E8Ve^?;$nawq3q z%BL91OIs>>)jHL6a?zl79YI(>eyXqxgYnY8mteXsxM+8cV>#lk5KX$JYHmkm`i74` zZ_Ueh2aLO1v3c0H)*|U_dsix9Lkq;uy7M zAyF&#AkS(|@J0VsUy zU8!*QQ)zCyU1pyYUU!-)0S6HhZ>R6!=AA!Nv$}nb6+>$0la$ei3zfd)u+>}P)6va1 zzI^L5SL=@hcpfpPXm_}+I(9#Pc4HA%c|w$^MP1o2#f!HY>Nl64bB$n zBOxr1`fC?j0W`qv{piT5zQ6%#x82SH5q^y&g2Ko67<|17RI0Q}l-)Uj`*|@?8JjCp zy0U?;ze)c28s`J4XA;C8Gz!a9Xk~?}6+)M2uu)@|a?EX#&otH!)WYE}}FFWw~u7bQU{2Mj_7y zTOS8llZQ6HCwxQwbSQ-{XYdz7Pb5&*ye~%oP>H1vgkUVGhF)BX^(^z-Rf9s3`g%*q_`2&JVpn=qDfy zcc(WILs5V(&`RDl@RP#`rGjbfMvh=9%}%GCX<2eDFUn)a8x+M({4|3pEf?y3j`Uu3 z!rF_>Y8JPrirZtpuQPTuPep?v5{0k-V(b13wt?={lKtZw#{j?yCWV<&h2?6Iy;c}J z-y5hoxY>;qZ9gjWb)M*? z`noU*>$Reo4Mz>9$#jd=^QfGUj))2)e^v}FBsmkEU~L^6N(i70&g zFGPjZCGgBu9#@-U`l8H31D~60X;-Z6>*E=2+jY8$n{?D)P3~0RVqPuiNs8KUxd{&V za3Yl(2EFCUc=T8LYoM?Kj#hkjhPQUV124Y#tCioOsg_$r5!TArXR{==&l%6pmUMsZ zz)9kTm;q3N`^E-CqtQWlaBQxbo}#oA=?-W|AkSbw%nYd*zJ1R|nFf3{je-IwBE4Qv6^?yh8e^xYfCP@adc#Ygiw)`r28wCjXRcDP_60Ik zcGhP(`o0%SOawcPKJvs6mh8i7dwu>)flsm`VCbRo*6M!u(7py@^Eb^@s^d0YAF7k# z7k|e|EO}Eb1u=bv#dSaoKJNvPIfaw}29SuEMv|EID?d$~Fxh8jk$Uq)J6WGj3R6&E zaT|IBK}JRniYt*q=Kw7zJ2-zy1U0IuQ$$^}IyrcB=)d{pfL2w&@)tM(DCtOv|@ajlX;Zf7;SwmZ{NW$>8TLnxL8xza zz>bR99K2#$m_9ZJS;?Kr?TqaU;~gKovTpfSF(87vUufGrT)AIW-`QfPb zfx0$uJLrn(>dR$!!3BuFgh8#EkN14eLUaK#*H#rvkptRBnR;fSNl6OeIo#cz%$2^ zfTQvaVQk;e1_~#6+>vZ^1l1}x-!>QJZwEb86dfc*m_Vpo`n&#cU&p~T6c}*na8h2+ zI_{*SkXMgoe@q91L}d)39KZQ3dI)Aj^HB~sU+gs)G-R4u39nws6qh!g;FN@P?bPp!G9Kwrf z?pJ+B*xM(zhU*bzqH+)Xe-P$%@|V{6P7^H@0p=glhCw8ZVZ4U3?1mi^@l0PgtPBf* z_dp$~GQCuh&k0Qa@wi`MRISo!#KxH`JAk!e21g08H14M(8X#+nywlW0s%R{s1w$zY4*(@N&R-kN?odEPQ7XOQ-qH7a5x^+L#BVShJZcs)_qi+=W-hqpvD9 z_IGFP{C0cfbAI35Ub7@I`k#an^SxQ?#ap^;AC{SMKG}?T9(%MMr0_tWe^2=Gv;2|} zKjYtBqXi6HZiiQIK_p0LN)k&zQ_0DiGSE=kG$dU)!(e} zLwI!Xn`0S`OPIgr2OR!2CdKVUPep63yzJB6Sft&_#p?tX_Y}J8d`%P@b*ADpPq5(& ztz{;+z@zFS;%nd(4aE>mgqDDcziYfvy2bM3-*%A3WG10g+z2jNp?vj2jT-c}7M+dS zgM5gJQ*;2J3=c*lden)`u1UU9f7KSkg_y-+QU0xj$te{P4jBEr2A2aE6vl>o3c`*1 zgN}(|h_uiEO8tD9u~wo}-DI1cKg??#R2Cg$ekWEVS{sE9r(A?*#g}p6_X^xrgcLq< z?;312p5w?MP2uANy7)xEFXoe!f%W2gwv%DP-Zb2`_Pp3~@aG?~QzoJ_HmM_rD^%lT zK>FM2&1N~{r&y?(5gz-ty`)I47UQ9}EK_AUB<1~9xEKqMR@=w3nqj=ioVGH)zxXdk zo!W9Po5KDa%}Ux0koh`A>uksVd2%IbF^e_U-bd?<={T|vh46}{=_Y8{2r(VD$h=T% zki?V3l^+MH)z>ugRbK(p6Gz?6sq6aAls^b;}Zje88xm(mwtP<(s~#->%9tlP|3 z5>$@{O%Ui^I}{Sg0{2ec{M5YFMO=^F<;mVcSmZ0I&EAr- zfrtz$<1I9S9$woWAUE_bS=NGNXVfE3we|p)g~_|2I8vfY(+Qa{{kseO9vYoCJ}OpD zC18BBVk#E>Whl72=!!ZTxn>$59A;aYj^`Z=K_oz8I4)QqiXTH`eu zrmHwE-_Ka|Cn6knw!m}i@XV~R=o$H&Kj)Mw`cEFy;ly)MEG^Kgq%%$AtGu5C{`T<_ zk_SV+;=lq@`>CL??7A>Sl*tB2g+_e+v;Nk?=R@>(50SjV*-l#!SC&snc8l4*#ADi& zAvvQqCv5B5OJ7GMLzA~!Cif@1YLK3FWIK=dItqmht_N!&{qrWuYg7IPdfjJ4l@C~f&&<@ z{qgqdMD9b#48J?V@Xr|m{Aa8N-EYBp;W=!^A)f81pvv_gzdj`tqj|;LN-CoN?b)p!bCfJYvtd)(6;8Bv&VI<7wS{_WUk7jlBut$t< zcDk!5L~l=He)ea_l;7#`gB2I`QEQ+uf!@&i%-q@z%!`lD>dNAiKoBl-dOyPH%4!}f z)QDHyVeI*mAW_>qKa+Mlkgyw%#r)xA{HL$w5h+Xm%FX#ArVHJj^|B1A**C2UJ9hI+ z+Z?*4=0=&KPZvRwY*N{pjd>++Nrbw3_2mSzxTdV6Jc| z*{n%q49paf?}30j7ho`=&JrN#FH=<)`rh-;Snr`3VqkLq%2lJH063|dh zZWKbss}6`+tqTOo0j&&?_cx~X-Z_Zoq$!BxDZI`?Bu_DL^w)|ds4X?GtI;=ZxWZjd z_j2gFO7tNg{GOAH3>`n=m8l^Mxd7)nY$jR~5Oj_>Op}W=I2{u5x)_~;6C5%{0x{l) zL)<(r3T4oHu{o=@ zoF{R%Bjm#tRC3{iv$(MGI&Whnn~y$leF3>isYo+NJ7eK?(Ck<580aw>$N7ZZfGw()~;hPNnCRrB#AD{YWzFs|Km=&T)Mg#7J>YK?7ekV6=>Ht zJO+XwrKEs#r!kIvmJv_ACVvLEXmEKL{%rVzJLmfMK1}J$X(eeiZQ{qm_T1T}v+PY2&5nD| z42I%|UafaD(|jNjNCZ`rhP(xb>DG!q80~w&{xlEhx4Ff(s=aa z9mJ<+(jT7NjH*f?uZv@e=Um?i&(znhK06_^@29F5J}lO6PgH2M{#<_4x}B2bMP9Tuu`axj*>OcwO} zD6(}P)j4fYna>=N-=+rAF~76P1#yygXBuUo4?-iHuXTOwij&kR-@$F|4^1A*v!@*| zyHEoHf0S;?<0LTnj-pXIQ#!^QaP=g=_?^N?BVZPN04%OpXKkIVcUhED`O*AV-{X*; z@z4o~clTD~gCUO##k)4T^p9p;5XS@zG9?n?`_p%+f(7k>7x)Ook%FN@3fZAK`q&D6`5H1pA>Xt- z>uBFCyc8#sdP!UEFHL*^n)2y`T31Ss7@0f1e7d}6FJ`_bwsekI|?$dCBc+$JB3B$)vlW zo4}(NtAt)l9O86wM0vbBGOzscRZ6akjHk(Ddi)~tIqtZN_deWn!)+&9+de2L8h#ng zi=r?J+m1b^_<`kQ@)8gU8v*4z{nF}o*dKiny(*LIP&Bl-HG2ckmiK$>U#CCn&Y_sS zv8@AiwJHSgrq0MT7#95BL|qQ7052x4yrk;Pq%rR@>P7`G3_34ydKx4 zUE}!HuaSw#@ni`kE;Z%#ruITut*__+OHu^)MLXSUZ$wN2Yravg1DGb|@3=dY0cb$$ zcku*_47z$1{N#6{%qlnS{X`f1_v>P{oeD1L1lyd>R$A_2qqPO{{g(FhpHiLQ?r+24 z*6jyAb@yy9p2}G*pI-TZoA>&{F6;x+U|XQ^y#U!=nc(i?Z9;Eup^sqlk$gyC#2fGZ zr>)bUV-uLo#X!u#Mza+>WcvARZ>}-uO)m3}>hVp3p?v~_N1<7Kv#wkK!z0YWmEs)x=s7*IpmPr<0TmE2cQ z4&TqZvkB;B^P4PNxoU^X{?A`Hyb;lQ;m81zsV&cGKHIfHh@Ppz6e|FZDt_d zqF7~STr}~j-g4gKDUZz~JT9ltgcT3i2IC0txyx_rZ4T#UJknTV`0bAvhp)x|w}22` zWaK1umM291G}vb9b=p_ZQT&aw9#8#%QsN6){Y6^B+@N`fR0ij&?an11?Q3(N7ry4M zN=~}yyZl?-&4{dULnzHRrBcZ5X=9~E-tSg<)53!>%dTdR>QMr&s;mpr@LfG~71Nkw zD3y|?95$%~mKfk@3K__eqCSEFjsDfwpy5`%W~EW?N3pP7i|}CCX{M%hW6OnQb$Ux( z75T9Cwl|GNlPMYzUFXPpUHrVH;|Wr=#XJCrbl5E}q5JT8H)?*Q@D+Fi#-e`a%IdqM zA=+8t6~(W&96D)V)GZt~FO9tx=*@X&kp~webRt=pTEsi@XZlMEBRrynnWJx zBwjcl&sWCKU2Vlv@Z%JM+U1ehaP$;il{SIZ;aA#o*e~p*dI|?CJstS2#7?giw+Nkz zxttL=6w7oM!li^?1ql!*-etYZ<$0Y%BpA3JQ#oI2tAdqMJY1Amo#{nkKE)hOBxI8~ zxf5IxT)iiqdM#2+t5%lTuFak<_^@uv_!3+82NW*_b$!D8E%SAmR|4YLEAH=>-hd|c z1uO;6GRgsjlMO=JyPT{T^dDPiWdaGoG(6M&N9d|L#hzeP1GndQ@3!F22EpKq#;C#GPI*a~n@K~KaR_v0T`^F73{h}%4Y01a>uh|p4TX%PR(FoD@c za7aU_shHA|+$K*2uzZF2g9lU6Nr`5Bsz=%wV;Z?hIu8lGEStcV|l{T&8vf&n-DPew$8jBa0Q{hKetN;V6Ip+axGw$pU1w%p;dQ19~r{Pl0I+5?=v zTKE5>V0Z|v*4v2H8&5HIuGj8p)n0~ZH+n38;X?fskg@VTL;m`x^cH3rOtem@bw4_vo!+9T=McIZ4XsK8U2uQUa zKOP<)p4A$rw5K0RsvV-?#5vuao9T1=b`zNlw!42zI-Ojjr(dgI`_Jwfl$5*oiQ5z) zuD5%&{=RwT@9Sya@@2xKgaj&jJPZ3@{P1sg7b*D|h`8))+5YEO2CpZW|66OI=<3?y z|9FhsH~;tG|0JgWPe@!&ZQ*lJpIq&E1>WDb1$n`tb|=%?`*L;73cv~1M5W>W?FYa= z;e_An3Nr8|{e2k_-xC6??@N9=!TW6%^U4(TBzy%2L<9>45mjq@dnNd=Z&o+!-u~vf zqY%S8e*N&hB4t$KrtSYm>3_TX^@(mL+*T+D_ut?1?`wf>Pus!>ZWImEs2$p`XaD%Wrzt16-V zs~-QglkDG%_}{kdU-kG`J$~3uf(DqA)OvaeM=gk_^jwTYbxNRTORy|Aqwvw2NU{x6 zd3Fi@JavZnl>ro9#na&a;BlQ=g0)5VJg^$vN_y@dEr$Jno&54Xbc zd#*;v_1BIIst5$FPCc~{#L&LBp{Aa#KCT?{|68QW_XY+|*jM@e)-OJ^<>2m)2RG4l z*nAL5z|!QCp3?yv;qG$&%lEZ8;eA(3T>^GE=*-!vjK^Y#2T- zon>Q;%xf?peQMg8`>$sgOur3k+to zbB#jJxIK3#5W~a9u#z|RT#sK#R;xyM_vRsuuBUy?<0XBxJ6RsvP zl%EUie-?35DQl1!#I^p`ua>7>9`>AB+tWg|TKzGp_{y=pCJN782?!lT*?fv)_G*36 z{V}h{C>${d%UrSU-kkfvarJyp0?XF6_&~j7!}mI7Gg+Cb>PyW?5-~HLo(PFM-l%kH z6oNkl<_1Df-=Rk^h%bLU^TSAZ<&w$jjXtZjTDHz)<`gKaVQjvfjyTwU&!b3N@WnLX z9CK`LMq&U!9lAN^G#?Y%hvRZk?KP=zv=2>{yViqMRW$A6u#Ss`lePxSoy}5XQ&q-! z1Mu&s6+2&kc$*+bC{kQ4iM!et6JxAE9^acj^^%Y8?W1#>xRBHZXI@3VrSb30q8r1c z8S0M24UPASiIS3FiGBpP+91Zg~hEAxG{JuhOj+IwEck2ZtDd?r!Ck9wk|*-2yeqGwt5%-? zMw26~c=io8jrdX~*Kc-|O8LIl-8nuWHGoAy`e_33LGHAucaPuphd%$je*`9QzD58b z-&iPLsmqA>`cs<5uzl}yax@)yVJ@CwmAkvU4kj7yFZ)OBNgOVQj?xt{>BK+Ph3?2q zpDeHTQc*rtF2}2%k}DUMq&aLVP>7KKMg^D{O5*26T^J?Pw)+c&YFAzIBlZS;4H>j+ zvYMl{J147k%)1@%rhQ zRO+oUovF1QSH>U~w%eF13I>*(@A}#`*leOk%wczi^x+fI_jjNdqSsfJTkdw_H?F6< z8xr2QFYHtSMTPeJQs>ws#@@?4m1sxmXvq4PjOi4E?V3Xxx%4>P&s=Qg7aCtn<|(T8 zW+I6E5PCkQ`K*GCvy3P8_$z)F_~$p(b{oUu%Q8=~1G0w{)HSLsrs+h&pGEluAO$fh z2-n)K4R?tgZ|zhc#1~uJ@6UVQ^h9R$=>yi9&B+W$t<|D8iBzN_qrN0P%)uI@&fZka z|7b%?>#3RA1|xb>)4h2&Zt=~pc{>MF#j`HGz^X{Cxg2lP?MzobkHLn0HRiw~(FG>s zfC-hvHdkj4IqcR0ueCh4imI81k|LQz94NIL;s7?`t5d}W6UjWsC5~o|lN<(}W}zoN zy>>$;uSRIr(@$>a2Z1Ic4$WpKkcpF77-B}FUYfpGdI*5$_mwVptxlF3x!}J;rzP5S zU55-DL(SL>VgFPt$@{<$KO|=R#g^VFc<8Ib(8hz7CP`k{M6s-_W3_Y~qbq2|B!q<+ zjzL}8PRss@J$lNVv)hjw*1YS4Q;Z4{WWQ5YcIzygOnqd4^X+Bkt=q~ z8^(O9YMdW^uskOhCok(+woEb&r9w8t?D#BHK2wx%*7b;WY#@mv%53VRaItnH84N-q z1gs48H;}-L-zyC637Op8>xP_=G$?_GGALy<7&T z@9nkw%v@LQ+F3lAmSGo>TA5Q58pZqhJWcf6k6pe&QBrH-rt>emehbS-Q8wqb{Z^RE z*Li}+DK%to&tSeuLa9?+IFk3?ZEaNQcG`Sx8L6rnrfk6e4&a40hc#wSXy9KeQ{sQB)Jy< z1wTXE&76m`O%0^)q?XE!whJXFBKLzw{@VC_??vsmJZPxtXxY=e1Y;)2OmPb|4tHg7 zh*-kessmMci1$8{@Myl8mU6K=-nNA8v8l&#fk&ku8CF``BNme}`-E2n6w`UlQfMp) ze$a25r1m@NEc$a-ekWr`&=20I_#Uc4u{3Jm>_YXt;GM2&uaLz~z9m*E6nmpm36|My z*l6JCbtUTh!V58*&xtFSXzOV{+s=Vk=}%>o0Bbt+@-L^OjfZpgep67Bg%t`$i=8ab zc0~`PP`P&J@L2NQsIuQ2jVuvHbmUwP$SX_q`Dk((mV9#``^x(G?B%^nu|q?z^3Pan z0td;i2P;9Nru(Hl628`{3p{|}O-$_XY5eq_3UIv8!k}j<4>vui`YYbFYI4s!QRE&Z z{Ncgje5FAQw|-Ug^r!rPNS%fXMUx?2H}<~G>enZ=ZkItK{ByoUqloT=Sl?GhtCu|8 zV?>||F|V`pDNC7{$G2p3u5;XY^(&@LpTyA91yy>^nAbigjk$)IO?7nCa*7UcWN1rd zEm-ALtIEQ|8KtVfXq+9kHpe#P-!sj-tnRXkIRW2_ZyDi7cYNIK%B}eIJJh5*Iw&xnV9Fj|!t|9oxt4Ls> zUgiJ_3j1_--1GStjdr_}8Ho*t(v;9L0J;_jX7}P6j6%tLl|k0IW0rdF@-*ft3r{2f zR`#^|z-A>@J@VVEhJF=vZRIgr*VT}drO!M3HbCqLkMUUm7a)`(g%LM{s1WjRsA^=h z4q&Hjuqh3o{-G8|rV|au$@=|qFD*r6PZgR+>DAJGPiAJ8_V5XYQh7&=X7=?dLkG27 zyOTH$&D@nZ0gfhmFnLPiX#I=O07a>8D^ZSt+~@(a2{xSG<$i{`^l~mPnEgWJe6SKR z?P#%eD%Z!wZV{IDTi4-9mx+G}h|MQ4Id-V$y{F>PhGWNbRZG1oSC$mjsNo8#Zr618 zMi;zs@qLPe9xK-xnlqHr6HBX+B{D>x)JFD+C8sl$xlC(o$MH4@kk3?IxIr>w`{FP< zOk;8$#qS5dK5xqs8}^EjMVh(-TRdbyi-F(IAc6T@)n>Jy5-ih;T_hIfI2NE4E4P%{ zmk7ifTKdz*wYY*HbuQ5w=C2vuf8tYumoUVg%sq4lHsoJoyb(lvNzl?Pwh>wbMW(f{ z4`!oE1S{e!MQ8YJ&6L1wB+F^X8M!`V{q@UQz0wUQWxxFFG+iEipkb%NF9KTUv{N_4AK4SN+c9hi---D~C=IAcdL$NbKnd*zY_|B1VqnnRS) zH5>Q$=8DSgHYeunpnwycDIa5!9y`mxddzs)(xmm6)4{B)}l ztJ&pclkT;d^G@Zs*|E`J0`Y7eC;ckpQQyl0<0zRUmzT4a7Zp8`08%my4Z)M~MrAP_ zi`wDrr!N~X(IA~HGi0&I@EjbEMXYt%NhsPr9@nX^+}WPAl`pGsK1V(Fb{l(_C7tFg zm(d(sIuF&Y51+zj|4>r#=9$}>@R-ueQ0>Oc7w*WT9F7`2k))Z~82;zE>Pm8p*q5|4 z(;l}GVTDr=bSEp}q~A?_zs19rrCeZ|wJ384liPf(0XhhMVO{nJi@n{Uv9x@3?>LfB zz-3wXv_P}6E?13lo9z-M3|zOTLk1EUVjvkdgY3-K%jCPMesVC|&eiqJ|4T^#WQU;p z)fH3!GY$C>$hT0_p#YOcUwLA>_O*63l>NCKjN@Y9dw-%)sf^bQ{FR*ArK_6)^Kim} zPLD(O`(mZO7HJuOoa7;wj3c=R>uJoX%n62}L@o*p(7SLW3ZGqGN?e3^F^NpfA071& zXY+YmTYP?>#^`Y+xGZ~fn-^VEyV@^ol2#VH_k$wNz+hKd&g#3=nrV*InY*f$>Bn2= zdgiwTzKW5^lRIl`*J+bI5t53U~d2F!n!zWX8!Ru88uzQ~k zmjMy=j%=ZW;E!EL+sUK46I&IU)0-Nj{R=!eVx1p{ype4# zc4KkzUCt_dy-H>Lb%u*m5X{6j7!^G%Lr8f*Y%$kBdOkNb;_{Ooi7M?M;rL`<8SW~-9ck#9Hd|LTN67(@_@$OnplHbPbot7xwb~kfl zKP0?*c5M?*$~dI~EU;3Uu2G=X2s?d!N(Up|4x3jMwhOgqoHS^Gt~T@y;g%EI=~ zgg)Nxnkihhq+q6Hl}oYq3a3ALNN*ymdF%z^JxB;cFaMJec9)|##az{%8l^y<9Z0Da zdI-Do)%%gMM<>WOV>jVAEbk+T{ijsQcM1%!=1Pvzc2swASK&s>?wi=!^H$OXCd9YS zeiLI@`jR1X@SQx>8I4YM-tR@FsaS#X0IAkgMO9ao$%u3;ojZrmyB%LlP6%DnNqSXn zeuCfY=@Yy%qamK1)W$M{Emm8YfQ-3D7t#8mdp_)oUrr4AuA{*Qtx01tm;l2#B<|lu zFA+sozTvUiGP_M8jgAMe2}3+8Nl&G?R8tF2jT&O5xP1EZtEz!cG)QvURD@JK$$@rZzizD_D zv4kPsh`699HC!d|)LDHbQdc(h-1rgX;0!`9C#aRzUK_+gH0JHN)2Anio#cLM-_Y{y6ej%xOR+gyI(M7ma6rd z0h8iH>sYp-l+)2V?})_8_X#!ap-}GS zxj1l9t344-OP-F^Q}*?f^cmJWgXVe4d9i(U-)#(I&_eTTxs1>{gRvFJV$NIGLym@2 zNU_p9YE#EvVIW;jVmA080WWIuDea!?G3G&-`Pp?qszH1Am+YlqW58GA5%d%#`V6Va$BCWE4n|r&7P3R$ELGY>Z?@m2|pM&%0eFZ#1Yg3ew^|js;UH zwqwr`FcU1)s`@z(u3gck3{GHo`58OQ&|XcR$Rt+12Saoxc(os#>`~);&a_l)_tqWb z9IZ-hQ~R@GeP0x*OlqT<4~43mjM=4d2o0dvtna^aPJ{A#E~jpLc-)qP8pZAtZ;SZ5 z|FDcLryyB9n~VEQ^y8Ty=@Uxe-Me!Sh!r7b5rh{)(blXM`Vnl)L4NyTisOb#KjojC>v_7IaM5(#f$&cyj)WxwxKTmBA}Xe6Zs(sF5) zjGiI9hF2AnQ#S)P+uYR+8$V767O3lD^S+9N>XBH^ah2(RcgrEw(d53V?s<~ARF8hN z^-wqti{ob_!VQE0zC(x)5&3^g|BwIk$|XRF)D=`c=^F=9x4b?*G3_%c9OKpDa^~Q6 z?~fiMap;858-falK*~%LHPs_jP9?^mD9Ysap;V36WYQ=!5ec+Qmyzfz$8n|JfMkk( zYp`ixN3x}nsvdqJNkF66uL9i}fs(C(;47&_Rtc#8w=A@$EQRzL!bnWq)jBsX8I2Fn z6ykNN%+j?#8}-MvDqe53_b}=%peV$<5H`I&e=J8o61sB{igU1$GhndS1Qm|BJ|E9t zjbt)f2;TkpNH{mq^GkhzVv)RBB)JTkMzuMFP{6Ar;tik5sZO3CL()<9B8Y zyW>q3O4XX0+mKE-5I!(>XR6Xv{=@t8hNqSW(Uc0pMZyc&chO=BlpWy{uq0TxTu&qq zR;U-yw%0)+%WikHwI=kM!)TXlVmsA+vXnPGC3sAy zhC!GN>`|%h8r^L_#hP>4=r;IvtsYE|rIt%9a53pe-kvrn5MXHQ>_YmH87)MCNZQRt z^$psOXMX0?f-pat45G#+{a9c0r}tlj*2rfryWNdt#+&}{DD&ycWW0G%ll1N50bPC| zbD!ff8rt0f`934`v8DEk>(dZfi5Ev3oGk`XiW7tyRfUWoj)h+BOjl=_ou4Kda>&wq zNkFJb&kt53K)a_ey~C5ev7b(}<^zp7!}v+kM|b67BpP8qQ$m3Lq0rcljRzwfJgr}?qWYK`d{t6ZvyZ6#VDmC9nIqc~*PXnvx> zAo%*VcFeTpA7R-9X6>xyRAEe^X64kX^Vlw!hTaRn=lb(a?$XO9n#`8Fl9bwoxz<>Q z=9Y8mp;xu9vv!&~!{9yi2b083c6bSJ*sjgd5_tr0LiSHjAtqBcPs8ruFB76uMMnXj z+#N$r@1xyWi!qSc+j7S}RDclcRfg`*I_ka{h(35YXqe%D#3MhiTe>&jfSl{fRcKel z@aUtSrQWOErt&0TayX;C`6z8pRu}m(gXBk+B<~}H=B=IS7!lmLvC_0Jl)o6#Syks@ zE`vbhHD{yYzzgFmW2Wg+a?VYmn_*G}sd8lBTd$5cn9RE;#-zV#`9!F(pS5ZqUih|j z{*=|^HCN}d%rTKxqbwUMiynWdy1fD5_6tXdq=tFDU7)k6-WIhQ$6nj|T0}b^f^C4i znfhw%WP{02d`R0%A~o*WLaSGXfM}B~xpYc&+3MWkf-!wT?%rh?X)uZDSDGq*Z&X%z zho8|_DjMirM1A7V`1#Ba8&5z5_ejG^9mA|1gHozUi_7mQKY~L(1ursy$L(73g_$<> zCKE{_Jzd(Xqfg^LsQJl04?HQC$t4*R%r5M}eU9pjEqM(cIdkI_l~z1?KdA?PIK-cLv7x>O|>^v1lC`2&?&w0WRfuqkv)dcUMketZ7n+Z z!(6)NL-(35g-yPmb(gB{&NWi+OgYAakb^VXR_Oyje1-@TB<}e=h>o>Vms0#iN`97# z#Z1k{X2o>b#qnl$H|i%%9(tOoVncJ;zG}&9^ZaSn+$TCn*c9%Uh|FUTgLQrD7D3rj zG&t6rt4yx%vt;~1L8c63a1_HEl?>_6TrBmr(_<^Z@AIp)^5M359d#0}ofza3IBT`E zdlIihMV+gvk(FKr64qD0OL$I1k_az!L@x_`wG2vR-zR9oGP$&9?ZIBa83*ji4)DB> zx5nvJYgDHk*UjowhMvu43b?MlZ&Nmp_n)%IN!WbRM8PWEg%OGu7YvQkvinkkz!FfHD)DHL~onQ5=>?D)cq}zcy6PV+}{GA%9SPtw~dC}D$YT- z;iGagM*aOhgV+2A$xJE7d(AhSbWD|`vYLJ(iUj<$TEiKxm_hiwP1f-{3Ik>7h8m^w zti?AHW%|}g9|50nh;p6Da&S*x^OCOoFJ<6xfF{_tUM>C+NB^u~>Y{@FI13Tg07hCH z3Kbgaa6HlI0X7r?12V4HS=P-)^|~(4`|)k81{#@lNe^3^&~KopsY%UNa-b7IHI&K? z$((wGip`V>0Tska6@(~v$5yyY>}R(LG*tyEy|$)U6j>#SWE_KJ3f@#=?&K5cBz98J z---oe*Md`Zl*Qh+Trz|wlr9~t_SuNk>x5uEKjISt1sLx~(N5D2EZ4XFt$VP@y+s<; zLZPKokz`U-JyGOQpglI-_F`^^lV<7~1UwwBCxz`H=n`A5FTifLVm2u_d#y^rZqN}E zgwcOy{3;|8^*#Cq*{Hfp#m~T$EP$wO(!cH;$7v4Pv z&<@7|8~=sgz-Ikh7o?L&NI;U9K$eqt=W(DTjE{qo;PdB6TBwQm!11s}>%9Ja^3O+nikJYbF(&f<+I8!f4p|2rDozXe&asgM5BSNg@l zfp6H0f)v#9#St+>|5rbL#}X)sQ-+Y%4;S&@Z~fDC{zrEL<-vzZAGP|T`oH?|e-Hjo zV)|F&|Da?4s>;8r@~>6-%fO#EA%pagUVB1QXJTg>e%0^(8zt@g_Zw)2Pp+WQs90PoUUKulpi6{U;ab&)~Or#Cxrd- z;rIW28aHNW9t&~^QnJ$$v+~$w+6tas~YBRQ2b-W@s~5){=!!vr+*LqQ>_0= z`p^8~U-kIgU4bL`S3Uk!k6)DHPgDH=YxVH0Zn?HSHJB{kDmz@!s<0oc+ds#Rq zLV&PGpvnIS7hHUQ3hE+Qh1a~isTc=AA4(BzvW#Ww-`kR4vqKM&-RX;DOp!E;644HS z@(Ge4p@9OY;zg}e+zY0Nfc|hZ3h&yvt>Bc)W1HUQ4zV5W%?f8iPTC1NFpx5;j!3=H9!`8qUFswV+hFXmm_ zf=w-;YPkAsATisYL?%UYsWa4+>hOrq51YqwUM`kSt5`?=1D{7c!!Eg4Jfq(Z2H4jd z35)z}B5X_=RHI0QLm-Q3_FHVCr_UONU})x?mhC~+uDs&S+^8KQ>|cb|unE$C?PJtz z&%ZqRAA^8marbg)7Lv{T>{0b4to?G&&VlIM(RZKx#|A?%Kw!!0Odmdy1Y$CYAIUV< z@Hp-!UCUqejujjvV7w7ceo^_%1c2wFz)I?_43A7PBW-@6#swaSmA4|}@|3Uddt5jt zJ;GxhKKcj{5xrfZPvd&IxX)}m#En7!AkSq@8m~fEfLuc@Bb+zp7>9K#Y;3TulDdOl zgSBxR$X>pw4c6wW{h>ngC)m^fGzoG4v}39QAM6vQZB{o~u(@pl&&~}hg>x#kABTMG zmmQJ&5#0TZ%;8Lbr`@az>?~1csvsWr*(_@+-aMiM?M)X$Iq2yaEdc=^tK}TkNcPZs zgFclS^K5uFFjI)w7~pdBdmbODJ`S&<9nc+9K+YUEkl1xEhwV7Z5OGQD|1hR8rIHJ^lq@I zD!@1>yxwt7JhkZ_3BiJ2k@m%rahA%^Zk6624T?ZXFLId_aWHI~M7`oo#N?wCyurIe z6&XVZBM3A*fPSi1Zi{;UOQ5U@${O4}@vdCQD>}E4xNu zwE%)2G-rr%WKzE$JqFvODowiMN`%GwD`VI2#ccd2mq`7e1Zg+{LTD#u?3)LCaVp9|6H)RF<kYfvVKJ^PaOC=}H$rNv%EYX$xf*s;=&<$>Vg`wE(LmYa0g9vF-@uU|Qx3{Iju zUrcN{d6E2qpckQE>!T#A*_7c{*QpM&!}%$O1`M*p17sZP8|~(_@qVyB%5HCF%0a2Q zRi_6`q&;A9OisW;G zC9YHMqwvVnfjsw2idlx>zxV^dA)b4V>hY(j5u zG##0}iSqLpXP3|AdsZ5rOa_wVV1?0pwkWdLZ4WiBMxmOqOXC?u4%-uNkDCYq*DWSB z-|0^@cFSN&WZ%$o&IUJ_{a0kSwStRr603Ke&TBC1PkUhyGe=LZ#N&iLL578zYn)|u zzZK5%22=1S_T;B6<>Pw-U`6dME^1jOt@?F`{sPsRThV&b-$2Ej94jrBqdSGEWCSbE zn=;BX#&O>L9A9^b+J0+J!f-G_PbYIv1#r4T@K2-ixB&1gTPo4uSjQCj+YFn_LA;(g zLZ6jBh{{vuTJf{rOGp@o$8g}YiE@z=yndpNeZor;UqiWJ<-RZ+YM*~fm;++(* z8V|BNTlZ$`8Sne%n>ivJR*T4FPDime!G{2-!c%6n$8!s0Dr%Z?E?KIVud#Zm5O2+3 zId_*M8%BP+8Xmn@tTbI{J6ONOLFYJ0uPc0uj9eyp`0~BVVSmJ|jH9!}v}5J17cFOz zOnt6^gv&(&Oz3b9^-wfrMpI@7XO=|m70Zg8IoCIOh8y`R#k|Mz-;yB`+5m`)#dfVy zYrULZ)MH|P@@%$#UGKU3#oiRXD}6U;JL!MjuC01uvKGpw4e3m6xLVa4c#6L`npr_vlzB*1~XdNFVGKfTEygW(l#Y1Myl%a3FIsodH*!4Zyqwp?Da{bYJfGDzao}+L( z9LPMF&V1-n(?Y?-GvBK-ade;^ga6{A29-))abpdyrQ!&$(i{ERx?}0I$XgdQVc8Y8 znSbvXzkQxgwteHec_{8R0`^S3ql|wj7VSv(fK}aGDW4zwi;9~W%Kim9tO%OP#No6I zcxU#ZtMpwh5bDj6f*lCiBDELVm6qeefahjWuqMWkF4$8h?cWuK``HJ1%=GIQAA%~J z2c?Hl$9Yk|0|Qc(lq=~Y`#Ro$& zEGCDBxiUd@J_hS_->OOYTi_GxCTy+SYC`)vbcLp8lk*$%<|#IAhtLw6%_EM+I(PxB zK0;q+EU?2$2pc-U4)q{?haL^)+qzCg!{v!E5b?)-f)d^*?r&Fy6F~`K25Ju#8PEqT7qyneKJq(u>9rnM&3^L@V3 zz?N@8Je5Kaz?B-F)Tzh8wIFIw;i5=pVx@ZFi|n2iGV zf8z3Dn9;|Y_nNNM6O}enw9{PIl*`t`V{Dd2)u8Nvt7J4)C><`#AI4%F1d{etd|XVlOt=>yjX8-v)99*0XUrtmF}j;dXKn&I)M2xeka`m` z1oRrkw9u`a)KFtz)lFTP(M+`F|F(fI!qYY|B`WY#K?nA<;o<1 zH~yL8@m$?|0;lx;n^OH516E(^&imr4aHri*=iJXvFQnX^<4A&@h`(Fu(e!}el?1OZ zt8fEvPn6y)$A$chx7U7nXQ%m6Y%xV+_9W{k^^9|$*P*WH?B>ZW|BMvqDy+!US8HR8 zPs7UZuwR$)=60e{$+sD+1;)}Q^a2W_IfUZ@u^Bnn%p`P3Sq#u*wKbryOYnQjk;~-$ z5)!#lq!O|31Evnpiwz@W>G#;oRrOq6_bHnjOdg}&BePRkA_#q8*A1FO5(xyFa4lZ!^?2#Y z0$z1n)FqZZkL9#_)B5Zub~GJCVH%B-oC-5&ZBep`$iRa#qj8663B=`08o52$ zA?kCP9Y%;1AG(=$IxY~WD|L{z>4F}UeGV5`-apzlo~53Abw2-f>vuSui9K$$m->ecf7Wt7umNIaq19)&NW_u@45rRgkLtwl1JrPH z1HdLz{G2A+dUuK*Q=!){7?LUHj1D{o-8X$!n|4{}YJO+24~=hclhhDgmd+mjft0j1)J}|BHj@Uc zdle$fNXOLi*6o;y&_2o3AGj?8RMZf5`^}1(ac>3)ofgJ1cg1}4Dd0|)q8kiG%N>e- zfIdFr?b(D(D_d;msu7Hup`0WZd>;BHwMC-W`RHWqkNZIQ!H^G&(XG%!xG@)hxH<6z z;Y#%tM#!1oq>0+`xG?mZyRPEvHPZV|A!={B1ZIP(?Kzwp64m4cr7X#f^r_|o`T#Dh z=N9?6Je#;P%<8B*2r5Y9hHn_E)NF6wH&<^nBWZa!n2k%0N@AQOx}5UKS`FwAYAHpW*tOk0aRmTh>e%Wp3Ss=g28USDjZP@O$@QeAMIdya)VTxD_P zTHhbvtY{hj6qe;#ry+ib?8(ixLvw>BQZvng^A=eZ_RCRjP@c$DqxBEF&_+hW9)zW7nl}wdjAd=*$#r|0*`O(r*6(RuHK^)m+ zmgH{BhO5tin290oq*&J9mLc@gv!U)#1>2eAvKY(xy02CEqmsD7S;@ri#D5fGx@&2R zwIAb{J6>HmY@ae-6tw7cVXwhs(oqCs^s^v!XfeD#>EDpgSA3|^=vkrzdAFzJ;PNGv z(A{OS%(+q95-g*ZueT7GW--YTJ~lb{bUL5V3xJ~Z5|9K47lley$VzXFLdkfoDRNF0 zSDoV)P4bH>w%N6VRciS>aTbNd(Lz+J`?`{)+jLmZPZCQq9d;73FR@{7uF5rd^5v%4 zM|maC=*eu?t}Lisug*&Kdrl4@UYowq?FYAns=cP09%Y@K2BGX z`@s|6cycK#0&K=oIMG^u3{{w<8;O1N>h~xdP8*0hb!Dl8+C}J?pVjE3@WMkC40zex zN?pD3>${Ld)@y#cejKcJiA^qV+DCV^dtK`h{`o#}u_=Azp0NZ{u4}&r)|un`>PpPX z)wzdP^|2Qn9#hPDb^(ZqRXsB>eMeB$(%0fWOMav(gJx49=*R~1UZ2WtT2P@$F5l-$ z97V(ot1CJg&0~XjRf@)kx{mML2(GXI6#c4&HPmEY;yn>eke*Exn7VnQ?pwq~{gsIF_d4v0{ zUxx_iW-g7FKQt|&k-*@czRmYxrOEF@+LKMT;U3a3wxf}RDGd$PmNH}F=qo;_y_=^W zZ(=i`_yd{02Ohp zWqs=?TjACi`1Rc|$7m4I=b$YcUqIG?a(pl!Wq$1~G@Pb~si4$lKn*Abbq*W3P?v&? zjTiR&r}G+_+CJgQy^a?1GtM9+@0K+-%O=P^sCvjw^Fyw^bD*R5BNE;R}+$1L}Q%)anbolKLbGDo6* z@?RHf!VR%v(dg#fi;j$E%ATrYvY&di0Ttq%F8zawJ2$G>dFS`cw!G!*PJQJnBE}~9 zTyLU;I2Su|7{rR{KVEMldzz=>M?y)?rbnUEKJt#R>`{ zDH5v)NT^ybRePbe}c zg%kiEJ>s5+8v1CZUf++)d9a`$Lo8Xc$mlfn($D>#+V3f9T&s(c z3&!48Y_;C7ReMeca9B`76djtvX{Tm;uM8VpkP>xPb}I+-iaUQv3VGQ#-u*KOwPbvb zpX5L)z5Yo}YXjz$y=8Vuh<#<^8>A$qCa+0j9tR@ulri(GM`fy5q@2pk@E2tJ5+3_O9_7_)vrizY z$kb_odLxyWGM2@-2L(=GKv-b|T-FPqWJZZ_j_!5*1M z(jI`bO;ZpCQ>UMZ6kEGMA*y1&%2Uq-j>f9yJ#Np>LU%ZvvvVZN)yuUlfRxEfe3|b~ z(AE~5Y`9DlO7_kfKD#0;l~5i5idV){qRGZljz^&N{Vdv7{#f;!Yw?89^h2a?j;k)e zxwVXo;7LnEv9B=8?7)iMP8R~YTA9D|)Fo%na~jGSVsr&jt(ai*v@7?01-OnndX=If zYby_O|2}cC@z>`^W*e|$cC!~n4;I?F&NSD34`TMg%@oTLve`d1bEaP#86%$Pj--+K zU{>_$eVTSf=W+h6gJB_i7Cqo}M?A2(e?q-{l4>I>uu;1=yp`@Qa&wKcxa`iu&w3~D z7+Tx;5pkpv0FT2n#PP;0^H9mA4C&#r>LupO@5w9dHecbUn&f3Q`m=oq5(mDExkWLo zSUXp!%QDrlS{7|>RjAqrf{6+|7Kz6~RD23Qj3Dv%b_Msr- zhu+2A?qis*>T|&mF^(+VVw@V{lip;3U9TC%{=z37aUorvu!ndq_kLE3B05VcX?>`C z)+Cqk{7v54rAw#fGQF8$E`>M|-*) zwe1Dw{^-pbrprbcs5U{FyU$rZbG`y#zq?_3M3cz~x-Un+?G5QDl=ipZOV1xz(;25r z59US*Pd1GJCAhBr?CySW58aEr4p4p?B8apb8xWUo&`|a80$rK<@?FgOU7UXJlmeDi zqCoAD`HN6;erf``Yi~%*eYG}k{9Y_EkB6iK5JhAgj`#$)b+B#?v`5OAJy|^SpDYnp zxbqEKPbP^EIL_|e_OGijtw)#~N8k^Ld#e>N(7gMmC~v<) zL(Pxcrk`U#H3tUr7za()k3D|uG4_&Qa&Fa&D72#~mTCTX=={@JdZJ6TmohJU{k7l-xL!-s)C zFEsx~Ed9V;p^3AUowu|jBHCbSV<~*4uyC``)r=pp3{xbi-zx|M1^rk>p}MdeJry$p zc-UdSX3jJ!#;OWc?=_UMw9P9m%T)|3UeChfR@sTeql>Gi6C>Lnv1bm<)CCm=6#021 z)H}sb>LxI9hGK8TFq8C@;3SLb&M;sN8ch&y&$QW0O5sKKig_pEcYd{abd|F2=&Oa9 zrVnm4m4dAW?Wv@MwuVht!DyO8Z6R*$dXMy&EWbjuJ6 z6D7u8m@z8RSMl8NYb^2rrQSHYQmIZHXxwiYSA`nuNLMYb&eHbDoQ(v25drID;7>na zy7J4dU!VN;#}C=0-ZfV!BSiMCg&b19UKh@A`$dE!SVxE?XkpCZ;nAYI1#@%%BVYBH z^5L*lZFe~p{F9AZr^G@1&y|OL0k)0)%zmEoYz}st)U_K8PG2QZ>Q>eF9*^5cYDBG9 z9)&?>=80KnPux{4cxkPz$8u-qYE2{a_o zrLfz#O|gO^rOoR%LaUqBjp7X+bUlu!ctalS4Eky5gKxE0JVJ<8TAsAr$x6Cywdca_ zIOZ~}VRKNrRBhCv<+iD%xcK{u1YUjP34ftsKYDZYvoMj#9U2En*@I5bN3U>A;sVPw zgvZ($uZHu>HBp#k25`&?jbU?J_o(O|&E6?l1@8OjoTZi`C@R`QFn;5G=0*>sZC+-h zc33;Tzx@Tf#Rt`UQ)fCl?uWh8hwjVD@bN9Ekx?jJ9knK3JutFM)AMsU9f)*mqvvFO z`Pe3oeyihpK}@9dh2I|6t0y0XCGrig|u%7jWHYMw-?icd^!X#P+iyE{3TmbiBbV%0W@Q0n4Pib?66f6JGN z7>X6d{i?*G=X&PRIR8l3ml4Z}LF-gM=6_sSTh zz^r7|Z*8S7Y9Oh1gGAqymt=TgGZ+=qf<11P5J!EIr;@i@<6GIjnf2?F>Wxa6XV6|u zdlx%Qq|-u^r-)!}ozw!sAdkO>!M?Q=6JNX-Zm6NiVN;xP-YI? z+xOVcN)g>^OKyK{4`q@lKAa6|etNP6wqKn<=b?JBn(oXF9=|lW%Sc0~VLf)i^Ps(Y zh%i{JG9|HMh>BP)=7W;$kT1LQ0DiFZrmyl`cw2HIwfu>$+G85ucbQ8=BY(;Q>b1+` zi?Yy!Dfyh`zC(#Cm)`SdxGSEoaBtEO!@mjE&wfb{lW3AJOY$81YNJz$3~I;3X$`)E z$GcHtwg-0WBUdf#4C%>wcKTwCH3my$!f`F6c!m0IjE1^p=4-{37NX>U#~n=S^3abu zhubr7C8*OZ3WGd&DcLP`ThS4MNIOGxR?@{-@ZKAEo5$f~$g>fK;rW&~F{6j}x`_M1 z=u)r4WUfhUzYlZu;VW?7J#Dr;aOTJ5xb6xTZf7P~g|NFNM)G!gT0$ou_s(_pv#;qY zdkF5tXnS^b-;h@+W@_|z+=vb{N^vRKE;&W57$p)^PPn?ExHdA3v^@qFMoWMm5es1; z`c2F`$>U%nS~K~%ZPbwkJ~_-KDp!l^JR<{2hOP|{z$EX!U0`hw(|P| z#M^Qiqu71Xs~i?Q3Jf1VorY_j4ja%XM(LG?3mPbSJCw9dzf!R= z*XDUDf1T8y|B_P5{IMTix62V^5N(;nU;b5O?d7GUpvBz5<2b`ou~M}Ri~f+|E(W;l zqq!G!^og{(Fn*^TR)3zyc$Z8fjy||#c0vl&7?VA?A+CEVHWT7Y^F9P7+;5|J3U4tI znUdS}JW}rD9wucqGNdh<*zKtQl%ghKg5U52VtqE5))d&0wP{c}5p-D7P0X5uUA;6f z;B{*AvhD=++oAKfw};N#S)-%mwoCHF(`AvNorj%q^zq9}%GzFdOWN)h6;ng%=A;Q0 zC0h8X;H`$WgW@5Qq`*z~VjZEvos6<_uBb_dyw~z ztVh(9|CU*PgW9Maf42aqWgR>^!tSm`+0zLZHyf8~9U)iNvjHf1kjrAu@ipz^ij}8+ zq~u``tz$L;&dW{Na_Kw$GE3xBbuPP|u6#!TM#mu_Bp!*vP4$9piMUs;v_ zo9d}TQq3uQ(BJ5n+|m&;9jEcRHs6-*s@@O(+9pgSqv9suH2WFL zwbjzgig?X%X>2|ccn3G7rjwjb+8CWCAgW!@e|~M}hTro{u|Fi)TH3Uaxfdibf>?e9Rh2({F!QchgNyR%Zm@Y*y#T`h?nTdO4y)eszE_hMK-!4Vs1;@sdfo4ct zjs6^kOWM|Q8V>soRDy|ya$IwJ3i@qi6+xq@Gj(H_Cof+ibS6`*l;GVs>S(!X)PNMT zCkjg>V|T5n(vsBk@LsX0;i#Vsge)RyWu`UZ`Bv@zkb3L>uvS$4f=L#_U)`!Uefyxe zXUeUW+bBg<@t_l$(&f1e{b=2Eb11BKGtv4i!K#|En`?LdbN}cEwq)0xl;SVXyabMF&gRvnB)q#;b#k3iM=s9Dea_i;3Ox_2wQ~^@r7|r$ z9pzHBaw(1eXPyYn{63Sebvvvxu9JNB*{xa>IPo3_pJ&sMHy=+hT)RoGp{RtMu6H^n zWK5Sw*6E|&Q~yKj>FKJK3ODNKaHGY$i`9%>4B-`fv)qjJaxoUqqi3s@OV&y|WCsos zLCco~BYm-bTZ;)A_EN#NgALPK4lhf+Mt`mPGlCxIo@)iFLfwMI zLV`PE0h4@vU#M>Hd5Za{rPsIsPmwC0*;5|4AXkjF)|l1?Iln%xz@flSFsSaO1J0G>hJI($-M2yHF$wYVNieZx@;`B{#>0!Q5~v z!ngP3^~a5|B;_DYr_l=6D77#wK`x_2Tl++hDFW5V7fNcZ2jjIIEUF6NEn{lKIzq?X zVi25#3f?ogHTAxs=T4-kpC zl%D?ZIG<7WrGytJ-0g#L(J4{*fnuSraQk<%zSdv=+u=v+MQ;(tkBb2wdh1nt%BuU& zu8A`7J#MRlV=wD+8qbQ8{lVwYan24*25ql>Frs-^chtIjq>z`gE#e8yOllzF$k%D6 z@H)j!I5S94)4s)N)u2sv~!xCILrB_(Wgf@Hed02kw7j*4I-5-~*vdK7(4E-85hG>uV*dzlA2r*`3sJ`SrI?VGtDYnpKraZ~l!OX-uE|{&F+> z^$~#vl&i{~=U%ZGERkVC30UOi$+brJ-gnn18;%yoRV+fTBRgPaI#~+TToIRO1}n$6 zs_5^^XKOVqlH|3inHAhg!M-E!co3rKB@bM!E$z*!8gY+nlzmu<3(CtXY|_<^O!a&6 zaSOuILHNNr4VrF~H*G9?uyTo(!W5jBD4bx)kP1O`aG*eKJ^F&v2zA22;TGA!%tOgO z*Z* zs2BpC|Cqd`Qe;D19Q)>Bo8VFA0oQ(Pr>)tpQJ5K~NJixFl(i9vWnNDkub!}qlFH5| zeCDT}h^UaJuMG1*9Gf{p8&KJ0vO>1NG#8~X_1wE?nv*_Z6E)yz2ghzp>pQH*~oS0k>NwBIbe5p2L zF9obKrrH2u$rt`L<^(H3PME#sCd*0neg$vqxD`}E@o}*a2Uasy4+O7kvG7=F-ma(q zmfL1MteJ_CC*>7h*x7ElajE$rRxs^NkqnIWx(?XfJ}0&N<+5e(I3K&76-lmgn#^a%j=M~o9iUPU8WCf z1?|+88bmuBzWlN@IxVzWqnM+?a=(i@+%QT^Jsy{`>+!dm!P#EjmIeLp&UGx4+O5X6 z*}>rxLxjRf#c9Wz;+J_;Y~DfIbM-ja3}fDXNj?RrE)8)wQ<8=s2UZh2Yp;_zs(4Sn zAgx0!uT@^6qWv1x$$=X)47HaS)WiFT-Dkku#3`aeqNtAcjr)NxDxRCIR916jcRpf4 zLf$w26|O?xkT`Hs9%rOP9ndqySimrlkU$iLXJTNf`u?c6DVE4-ylUt|;%-qVhMn3= zPb+PM1p@uJ;N6~d&yaau=7a9P_ebEI9{KVm(uU5hw?bge73k}wmFl+6#=q>B_Thmwsc@Rhhr1?5c^6fL% z)fJ6FH3%obJ*wtsclA+6H12|hA9wI%1PCUv`7%Ccw<7WJtBzXND7!*stE577n)wk^ z6q8|)ob8GuWF^d%QmumFET*fT+=T-hGP~?PY0FEb>8$m&e(iSfvdg9_ypz4BHHf)h zW3%F@B!SI4mJuaS`Yn4)r#VL3gW#^!P7H}rNH#5q*5%Z{8GgdBY6CQ|n5{fAv^kAs z^9G?3-`T8L?^HA6+_+h5NnutXS2HiR@467$&u$6LGmZW@ zc8ME}fO*gW=mLm`0TWMAwW;IETU!q?19}IQd)OnDhltIwSGX(jpn(T#clC6;`xBa| zsPXnLMY+PM0fuqMTByOZOHJv3j>*=sp(&sl4UdgS=B$A`7*^>%kY9*opS+L%bRsC) z(lESNq@)J%t0Wr|;3+dzg5cyW&_W>nkOt`Ppqi93tDdC?lRl;nRgf8Fa z-Kx`HuReiMGVaTcD*@TKbZx7?pR?8;wAEOOu5*@I!r`BPKVla)$p+g@sBcw=Y)=c? zw2?Wr)o#^mS4f)T$)XPLtOSKP)`vNz}Wx{rC!v^3}(FKvONd3rdt=B8JrdWqH z?4k$6J9IJ@STHM7Q*Op*tTZZ&q|deI5lr2$g&vG)&U&A0GSM~Q{R@-Rj&!`{H38WT8S1kU z>qzH29+|U2KsH0XFGJ{R7NZl2!n!!e%d#1q-nJqK`nx;H79pIYFVm}#LkT-=j%e6X zr~PVS)p0cO>B`dRT}>N!+@m*i9sDctYg_)_6kZYh4l7r)qLM!3PB=u$A1o+0&AyVL z?o=$Fan;oG+d#3=#u%*;Uc+f;bPBK4RU?^)#l_uAcAF_b?IWtTGckMxHLvi@A9v^~ zfy(g3To2&eUGESc{xC{WG2z}cV(e3BbI6)yWEB^!2MZj?9gFJK$jBIS)o0{MIXn?K)gZ`xx;QO@^t@VktxUDetPNxWk2KoAu3b z-b)8GIZ=9kc}+6+4#T2iG^-|z?_m$Avi1nc0 zS>f=08A#jdZdbw)pajxdbaT{us#WZ_QHUBc_BmG=H+*`4l-5+p5;gLylrPf!pb(acO$dHr$JKnu2 zJUu=GYvEqfj`JA$EBvkJ8x{nI^_%RKE0iSdV#XL3R49ue)<9b)?oE?S|5cxTg)L9` zoS;V@kx|%H%J!0n4_KNo`HqUR*dg5t(Mkw?>$0etzXHGAD}~!6^89T zdaG8Sdh(u%v~kZks;gt1`R_OE7vavAuFySs!jg9Bnkvx+;q*Snb@p1$c20vi^&%p} z;rlpV%_BB*S<8)5My9ue7`fE*8#?QX2Xh(I1lJ4|?4c$e@Dx~Ruw0K9F%)Ga7JP@5 z?hFLze7U*G)k zU4MS!EkgY(aqpLFicbIgQM#WS{pqmW{_$OZjpGR=-mPHcmzQ&S|M#O&VCkZK`O&K3 zyQTl@U4QwP@58SH*s$d1!QZ{A|NEoQ!78@S+VctW`VC^f zd=&Yj`1d+M=Nf@ZaaUS@`2xTkqhL+{F-(8#k@p={U@}F>`7LPv(o$&6;(ZMC4>3f* zioZbzqD?%fuaVh)Xfu}^$mXWs7TQG~*6}WDtu^Fs!ja^HKHFJvkT9WEc6P(%|E;Qb zu5=R1x#$8NK&#)W*frK4{&Zo2*~CDl2i`Fy zqdLDO|Lt2*G{ExwyO{s5-M_Eczl-^=WL`L5e`|?yWZF^e#j=%$!FdQpLP+RBzv4F{D7r0+&pUGa>CnDl7=f>rt6=JVI!M)X$yj>H6E5 z0ea>=91DaRDApmt3nt_}`4!g#lH2viH&QZX#{K|lBxL%HBsUVUL?kWb5Z}4o-r@we zf@2j~_kXY&gIBO?U3B*D>?xG@nb?iER3FXeBFCf>5@dWp-J;~>0Zixus4*t-xpp%! zN@wC%zoAm%XnO7o^-od*JVW(XiXt@7=A# zDy>v`8SXn5*a7an8{XujsY#T9E^|jhw!yPQH_SDY4(n+VCUc z4GIB1o&qJ(S1KO+FVdZgFpNGX8#A37!#*Pz18g?=VobzA;=RJEE`)8B)y+89yb;q) z_IRY{IhKg~$;Nio`VM_MB0Wr4Z6`I;BCnvrUq4iUE!u26Gn1}8?>ZmqFw$na(eDL) z$l-DmB2BgQ4aIFbld4PSHg{DD82G7de*X(v#Mt~2ug0OYJL{}h{|EH~bwFix(srrg zbCV}m6L0jN3PZ7D^RIsqp9xTq7PyDaefUYdCR3h1jN}nrw6^Ytv`wq3#$%h|q)Ni5 zdgKAG%`RglQItg;{0+!ajT1(3|E;B-ythTWLPq)f7hR#)_Dg2TBQ$7%j@nYI9RDk0 zT=OFNsp%k(5XBr|cXYm`z*&r1_ ztFu{L$!f-=FQeC6-v5$rbBHTS6y?CH14Hv9g4#*XSy@?bw0p!pE-?9H{;bD%XCwAc#MCf4vnu)ab8Y zsmeCS#G|aSGqS{LJW@o(2om1+;w-NsCfvO2A*hT9kU3tLvl&UnXk#etjNs>VTpy`I zG+@lW#*?AVaqH|AZ7psiRh2K)>AQM`l6ta};P3xLQVSeSeF|OVcV`)hXlB5tvKu0a zN5cmCc`wvojwAbiqnI7x6<}Hs6a?urWR9rBSS+T%hK=y(js~ch=1Un5x#Q64uOP$A zZdr@$c8BzJck^x$$7s9C$*FEm&-hUUP=@k6c8W!6I87GXY(WCI!t>rgk^2@BjO8yGK+l2V4ryMYEa|+Av;a$O&;1vC5vrzXRxnTapt5$`N2@n2 zB^?ZSdHE%_=+wQEn#l*1LW_imli)k-)J%2DeZ(oA+h<;8Q|^siAIDMEO=&$HOi(t@ zlMRIXXntuo$z)ib+Ekj?+0XE0{jjB;tfc*6bQ~XFp$_U5L?UtZ^EDY@s(7 z!r2-9=<8{R-QkR%&{^jV@*e26yj*?tQZjug-|WYC_os@bo+2@*Y(Iq zuKM!1b4n5oHm8XMr{E%JVR*~igaFW;gdYSZppY*nwzrri6t7S|7t2&{Om^EM>fuK` z>)n;%K6=eHKYcl<-4=uPrL|$0ot`B1_S4CR-393g5kRLyFB+D@wT>9KT<{eRRF1nph??wW|O%u z>QG}r!UokoikzAXRUXKNx2H3LIQxN=VFFBEiH?0Y?t;Q5hBy!Xl@Bq2%37{W4M<1l z{2EvI4l8}p0Bf>yozp4mWUJVGoFHgZdAt$i+Hd<@!wKa-n5fK9lj?})JNq0&wXeW6 zzfpNC)SoFOv6+7EhobY(j2ejd?uhBA0ie{t+NDA<=587%w?7Hoxw{K1s%P%`%4KSL0cmEa!@01-wZd8> zqkxAzavhB@RE=Gt*A(}W!4p8lE*?};Z=MT-sP^T8EioRoO*MuUD~SI#*WSS#K;|^j zkU{nr%P(!ma`GUEU7B8#V) zgn13*xJBL$6ywAX&c-vVS9F~X@Di@{d2Zw;_SDx0oA1}!*MbQKF-aK}!N&_RBRWk| za|u=rNf~QsHjDGJ_le$OT%IAzvdfgOO;`D_OrMAR0=ZE#;mKB|%s~y{1`iB;0`mY# zr{CBG`#LPL$Pv%K{3Fnq+XzKgY-}itzwd$cXB?{^`;7*IIn(fD>2x=6D)?* zsFjH6+*J++?5>trR>*V##R)oEZ};Bm`B@V#^#A=e!b&TS_zSsJFO#3|8qWeHC}>w ziQI(en$&31UxdAhoYSSXq6T8nDW{zx%gC~L&=LNWT0^#~{-7PJcs`<_v_pzvxKMB= z$N0Nb?n{eVSp;s%h2(!VTZrOmophQc-0gvQHCS3^Y5YVr=rs$Us-_$5(F)@u@uRS@ zms730d=SDe4JprNARBm@v>CUVG`?AT*2r~Gg=Y)st4-Ma52bnFhiPmWe;cM>I3xjN z@P?a7P59htNBd43*4`jRZ$XqqZ^>ue66$)kmD63E-XDhf?=n?YWgmh9gf6D5^4jbo?tI%mV$=&2Cb+?3 zJ=N2ngFwQ(kkR#JeCq6W6LxnDne(uR? zE(pQ?SzFhuk;HeSQgZ8v0TcLA2vpzaBs=v!#!awaR{#nNMT2J@a^Ek>a7#_WF+&z$Ok~`>8wU! zvkt%E9da&uV?PVdrgj~0H35~sa&FQI7B;NqRlnTgx?fRPYdLJrbw3K`G7Oo`h?L3G zGKdI0L+;@ff5zO{3P2F-^;^TJPXMNQuJ)4xBXrOVA*J zZ?E)e+yb*<=v}KCD%V5@%TXQ&Mt5-3ig%uqbzGyIa?7$pXYMquq6(BL8vXG(#Jqjt zD@^VLR~^T%&DHHPGq~1~N2NOC)fqX~+J(DJODJ7wP200`GR z{YK~qeSYWZdq8U*h=~W9>29DHA`z67=zAS8HZ;tomPnWmP!Y4H``zzzCa|=s-*+^x zMN0b$k0Lc-u= zrt`>Zcfn){-sajumzS@B=G1`#A7MgOuBqdmHL94&+Tx!}EbReuDHM;StYie#OL)6P&%z&N6n}0M%jI8w z5$XKNCnMmT?m@=|Pnz&z(L3d_Z{7S3>>H9qjb(O^Q!7+P1Qi>vV*&S%0H)LzXpE3BLC8?o`*c+PYNG>+aUKME1d$MY!FLW^+~9q*&kj^EJQ z`0FEW54g#qQL(t((e_MJdcSE>PN8AH(#oy~IQ*mbTtu5dN}ARq&|iaH+nun| z>kuV$vN!&|XSS9`+n<$E)L?B0FgQIH9Q3+xIt6F$md7shXBqhd^3*+^!a6l^JS|W7 zsET3S2Z?rtMk2t&NUF_Ow%r_zNeTN9t;Xhgypp9-ESQzHYqAn#-=JD*D7>&TombQl ziVO2fVYcGwmqZ)ub(^r#?yn48HfAV|G4UldkzJTwGGnK-d-D$GgtOgPmlfd6DVUeQ zMMmE@@6g+KUVE@Z!_K>4m{LW%TgT4cQ_vsk~w3-i;S=Cyoy8dFEQFn|NsO}gsiO~$Y zgtek|S-_b_z35L*WNB^55gnE?Sm4Cqv?9jBc9q*3xxAV`RoUFbWO;F z{hUMba#~O`xLI-*PLmw$gTs9v5u4U?j48qgpI7MJ_#_yZoP_O)aAi@_bcl+j#RHv6 zk%-zYy(nq3Xx=Dhvti34XP`ZHA?g0Y|LFzrJM-#KRT9ezyh6iZo4)54*Y0P`d>6eG zVqVKrdy~;lsRW^v-Jy7 zB|GV&kuP6iyh)jkmwczw9%ypk;uF}0T4Jf+nR<~xM(6({)9SThK~NDHD>(vEYgPBz_UUq|K| zhCj@COIrnhV>TD7UmJ4;Z@KBOBu#oOdGkH&jR(!2Td_+OdFgD6_MY^InR$rl18+b&^e!h8 z`NMAb0EZp5KhSTm_Q8z|pghI< zO6skW>|Kf#(sZX=US|o_J!I%2#CJEvGM)$x7i!J|tuIt+_~^|1gWH7N7BW0K|KWsM zr6~bXqL(FrJ;sVPYO%|&QM&T)7?XN7_xi@PU2mfx5+(H%lT)2(^n#u+DXo+l>dNIe z9Ow4d4*Q@(`yL1d32>^d7gPKKqmRVc)#~;>mllK2!|p|p00J^ST{2{p>}g7B{i#9N zf$SWr#-fk5uEcX0Q9oE4f>`oWk@YI24lWbFH{9w%`?@5j{bBG=(>VH{5DP|O#*(0Z zs=*7n|8W^{Fh2UzXBh|*Un-vC>zkXPwK6%q!#bC02Wy5ZZNuqK9__(Pq3~5#k$1+fClA$32izq^f&y zOPWdlm>@Zx#RNw#3uuyh$ll-0fsOV}LS*g$Hj{bjMksvKZ7XKLB}+R64URl-Cpb6H zZ`)|eM-iLcJBRL~rwoq7U*LCrS;?;LZp(|4^1K)$sV=`dw)GBY^Donf$1!rkl@|Gp z{%O)YvG=Z^PdK_NG|_JJh-zcfF`-~?%&hPst@1g&@hSKoTAc((D*d2NG~NpshQ@{) z24>=}kDq(YXG&Z(vM z|77%ZuHF8xq`2(;e;~!Zv4@*AH3Z^EaCvkU-po-QllQ*#m!f=ha2nh%6JUw4t_(2J zbI5`5A=;Vt3NAzQ-!90_Pu?aEWI+kjX7y6wT46mMIyy1gds%rdul&9N4Ms?e*`0gk ze$da2V-`X#-k+$K4wMN&Qa1>R=^-4Ef+C=7y%d)Nx-LA zR(-v_A(*B&&w`{PeB!kwgH-4DH-?RSXPqGg| zQX^Y8<>Kc*ywq3`EOCe_m&2R?`RGIZTfq+pKFWM&Ise;=aiB?iB99Vz{x^3FAMjrB zO-Ih9nf~GJBxurJI*D}BMHu$GRsa3Vm_Q9+K3YF#6MvZXAEFq7Chf`H%<4X;ME>DX zU$9EPBZ6{@xS8v|3-Vtrd+yw4JWUs z&4uv${fNDHRmr?`+1{cOBnB1`))zFnjtC)0wug(#jGyyOK`I|@`dY(!)WeuZdWc#N ziDPouJSVDh(>kX>xKr9T$F{Ii18SrU1r&~AJ0Wv=;l4z4xm*9B_s%JeY`&k_|7Ci8 zzeV3EU+|}YX>@X!a=lxkYS1VqZ>zRi`vL{aJHM?Rnb7UkuUeU2gkqpw`DR82m4qhv72z66KaVJrc1Oc<^;nF#)4{EopIXb91islV!&xNHQuj2_vW$iW^rFf8! zZ+x_?AV8;Px)PXUC$}bC3P8KUyw!;s6cVG^(rrDdD5Z9o`mPl|k+!{O!^C%+K&`AZ zzJZDc-5LyVifR{2%ws)lw;oYyg5DRDA<-4z3m0f)^!yx{oAWoAtL$Z5Eh|(gjmggk zZ+>mv3K4^5n~J?l^$31^1nkVZ!n}i#lO9uM{u1HMN2ILQ)1t-jOm+Ny8mH|8 zbt@r&Y}xshdmeDDC02pYC05NVR;X>(7&+g)qyLqq{;EhQ=^b>t!r;4{0V1%zRE zGRuGU6S-5h3ELyfCGwk!(><*EvoUhe*3;b{-a)m)4rVACTf_&w zEZY=omx4Xh_s(G0JoDLNkE8HI!MW-6vMs+D*T-7^LD(a#yOrPbrt_}`FYYfxdfFL% zK{FsM(Jd{+fQr>~>G>2nL)uW<>aBsQiVB@(ldNQX0C|2AqS$St@Dgf^Qtm)4yxeaG zJWgR!PJHj8hGU+g^*U)eyRixrJZ3(~y&Es=KmUyO!X!&RWp_$+gb5r!XgHf`GiC$C zSNrhjUekaUy(yQ>DO*z_O-GL#Cx*_Q5SY8o@mEo4`MYgl6cnIA3yC()$>S5=&br-h zyaIK`XbU5N-_WdREzlu6*6N*S7KZ2YilAMuw_6RgEwx?5!)#wfJ-Xz*XJ_9?HEhsU zV-_|u7dK9(r=!QtmQA(}N2@dxaLhV4E4ttG?YSdcI(QK-+fdh{rx0IVt+NCxHf>q5}U z!=kL@P$Ujlm3bwa`Sj5ICp568bBn_p^yQ&tLDK|xDR4xgP67|dp`d$c82?#=ekW_& zx7cSogA}Rlbh1M|2WX*myK=(D>-2cdifXu%t)sKJC%IM#bV7RG zJy<@yFMym)K5>gtgdUe%-(ciU)pwQsP!$!oMAyaTKepnz_w*<}E6Ks;^lh;LLjuy~=P-|G? zu4MglEKvY(#ZvI!yokscKJYdn3+8f6pi19JvumLW^C0(mgH(Ig{}y!Mu<@Po2ec`Z zufY+ak{Qz=>lIkqQ_VtR`tbfEXoZdt&smQsMW}R=l6dVSOSdhzC#NT{o+y51V!3Ex z-OINCPWO|~`|E&!!sCq}ShX`YUc$~gFjUQLn8J_E>s-`%<7UvXQo)cN1vV+^@RYMr zMK|_1lIpQWkwn419Sw%(sknw6lB~Lk1Tw6a-v81o2=!W_r^AmaTFo{<3}MD;v6oOX|8XU`oLYgrAaIn7ZfrcqZox_w4gRV@wz zrChh&w>ElmYEXJpucM2z69y2{OTtvXlvgNsAgJ>>xj$^G15s>rqd)sujk$TbX^^?T z7Rk0Csasnc)lD;Fklz2~X$8ION9rnBW>P9S;jtt|Ws#itY7)Ws>opt{M`8V9pGtXu zd!${EiDy^yD_kIe>WNO4T`)^h0e5G6wSvV_2xzgA;ozO(xd^+H-U#n)^Fb3WM^TRJ z-|i`?q-ruiHO5t}>Ky<*xsSHe3w5-FW+_lg`6P`;Ym_*wA0VK@rv2y+tRO9A>b|p* zRs8K4mcdq~T9$P2uvV?n&|&f;C-Ko9G~r6ibtUR>K2m0`33QtSnuHY1*T^4$pN*HF z+7_c+*QED^pP9~G#t|U3+IQn^IGyc?r7Ep(u668a1kTuzT|ClOXGMdf03aLFp2VoL#5rJ7@v8(wn_+#0u)6mr96-LOgVu0W26iLH0R#6Aq-=0|AMp@Ak~R)t~I9 z=%H`xw^7*@swzkIJI_j1=~qS~8_;HINxLTNd4ffqR+$x4qcifz%Ei^@bONiW6&%0V zdQZ^$Dxg%F*D>vkpYi0Qq;8RDN+4*v$mn*QvFT@sa;vbGda=7OD2NLZZ5%`DS$5PP zKiV%M{BYq@7=RF~d=^;*9p2<^0V?nvp!7)3aKaM|P_?#GqclScC9ZjPn^%>ET^r*o z%F5c%nJudx%XOUN)itZxT3m>bU+0Y*kC059wAWS;6JggpNZ236(VnoYITwIJYLO4G zfr#g}PW+K@JcJx3WSAKYqwN1-Yo4a*@2cAjqZ@&lAu8uz{4wxd;oaT}hn4yNjd&EEt)0ju1p$rhqbtNpIw zJp?hq-lS$$q+L2mEXjpfXa7uU*QsS|IT4UzTR-2|(exC$!ca%>M+G5(d{ zCe_sqDdyX2;HcSt9iYqd-5-K+xrm=xisKd4ow^Ixvi?*&zUh{=5v|BH9%or&($qqf zL_w%o#^zr-PVSbK`sS2&Sj)q}`80s0$)+vk9W#@qXc2u;)dNZ1E{>Vse6VlEXgUhR zRgPL8oz>lEsMOehE6INh-AldxAB9K6mBUsn8VV=ttW3r)TGVY=Ee#uql2BHiPrpU1ErQ1G zltUPUyND>1{=p;a{E4=6# zHJ^Ezz2Hk!#N{{$DZJ^$yPqGpqq03hiBMwP+V~7w>J>wbl3YwjbddYGjvGYug(Oq% zy9K#zdb;I0&D=}LDUz(Vl3jM~IL$HRG1N2uTx?G(6tP4YO17V^WZxjG-wRdnnQ)&g zCt#L+4AWb)-ts^g#VlL%BQ)%M^9t)YZjf*Yicj7Xw8R?UI$VkE^@VUgD0la3v|wne*N<-+_cdjbX%pj$INM|37P_c7_02z=vdPko;8if|^esVRGDZsb zLmRHe*X%*V{P5Vz^v`fr{||fL85LEwb=z%3f>4N15``iu84-{yAUTR4IU_kr20@fi zWC;RFl8lmbrWA@SIcKW?$vIPmx9R)7JG$-u^!|B2-Wd0vLk@>?_E~#{x#yaTELPWE z=V1LtcY4io-gbxMZ19SDp3_P{90h9_U$50mKpQpbF{>n;-oC<)mR;GRyK+zX5YgRW znv5?mY5v}8%)G&|UtUEw@4MH|@$GBx!(vN?x8;PT8**=`=8=irvA^^RI(Fc=W2V|d zpQd39lR$2xTUG7bmK5HEIICLDzUCEe#TPz~8|qFb!|^)UZ0Q;6d!NAn;lRphl4r`3 zzm)z(oNZFo-4&0e4Gqr6Vz}+d2&rcY_WtZ%p}iG-*Pk8|TBBdI&`q!3Rc&mY)xcLw za?+-MZ`P2C0<%GrW18l*DJ!M%Lr(if>&s2GS)dR+;nUfE$AO?b^vTR$dCl}|rtv*i z^$tdDe2jgADNYBt4b|!k>@hh70e)wgooz?=IP=UT&adotN1vuU=H_?zPAe%vPQS3a z&N%#If!Tb`XLjGqpzQ_aM>n?MZjMI>W((n{MmB83O0I_ej!(WX4f;+fG$fA0t*Wkz z&(hcWtaNap#zLzpf@ZJoxm29v@wEnLSr)RGc();Tb54pz9ot&cF3dU%H4bTPEXpa+ za+-y2vsLm=xGz(*y=h7=y&q?F-vKzmLd1L$Cg?=Rz%Mx|^GT9nPD?K+zbQ%}B z{Gj987Xy(koDENji8^%ENAnqzF8dW7M{Hi<(xWZW8|0`J#%Gzm0_ zyt|=WtOJNp2}qHoVoTr{!ED6!*OwdX?MxHOkdpeh-ex4TPrGnB0-HLWFhyx zJHWRV-hcK9NPHvbw&Ud9=Yu(>yLSD0?x@J#V;;4UJGaWH>lxSEx0Wnz{rkI)JlthG zs6#yC73pA_+Kz3){i%oF6Zuz4C%+#y(v2$$V+<=d(BA1Ln{!*gx1BhaCOA!*v=M0+ z4CQjq5k&>0T_3M;6{v?(Z54R*Es98lKsA`d$y6BFl6ejX-Kpyf^UIMW-zp}YT^!aL zsp|#0*`gHqq}83~=44;pY4m?_V~_u!(z7gR6|q+96ld8r+LvswC%7=Ob6^G*jh|9~71ql+q^L{AIAi&QC@MDtzY=N_{=Nk7~6HBEBkKHgrkyqoFQ zdUyUc#J)YE(+gqZoLwoj^X+1zL>m#liu`?5SCk(~!+HM^y5uXHmd$2>-HvhqNw5V7 zCaolZD4`lBJIbCSmjDY1ftuj~Hgegj7fr5@zBKB`APRiT|HZcaQ5~@u*`Z1Et*d@G zUBX%FTo|ho|2vtCwUu{>$IdGrpHF~w@`q+Oj5y@2#gk|cbM zhIPzP$i*u#EeW-q`&1zd`C_=737%tzP3#Jr zXcIE;33P?Q<|7hjro4I&Bu5rqnv3UqN`al1lb^ySAYymGinx-Bz|*V~9FD&%?n|6{ zWC}ogC+md{l3N-Ehw-E_H`(Ghw&k%MjSd1eS4UO3 zzfo93XnJ(n(djJS#A*o$c28SL6nqVYn2XSHKu4@vigbJeA1@y%M`Py4XQM0-hb?rA zScJpJ`CFHr53d=Cv7=W?6mabMf{uwUV^-=+N9Ne9*&@6_-yMP8lh^UnL$jOt+bV}iEp%7mQ8>n zGr^(Vv0`k1X43?dSTRkS`7U#na{9%3UE$c0W$b~;>fCF{U<0nlYL(W=Zb3$}_Q^(L zc!TD0t{!1IJSB8K>W)6Wlmt(pY+1SCv`x)!CWzXL{d}7$@%(3Q!*>0heu@m4erX78 zM^x3~0Hem}4Ml;Q+#?Iaqc-*@h}a~&C`BQ+YQ5F>ldB+BAB1I;oNemA$i!1r|PhpPcx zl1wr6V!d$((a%xZ^NytCQ2Dmu$Zh}Y7A%`|+fA`LDgvGdUp#!xBa`vQw1nh!X}uHW z9uNqdc(3ABWT=uz>lzo-HWV*9A0toYgcCU?y8^h-jx-%?HO6taQ&kOx2Gjng$=-Ja z_K!Qv>kgjxJ;a>ce|P=UMu{79?fqLOXg!yyWXfdM)Id>Ox35Cu-;7DaHXDN35X3f$ zVJ2w|E1Scs4OS&x2?{Bj-?i2==aI_y&;wZZfhuo|RfLfBO+rIpfQ~NhA)y#`FdfQw zBo@jHf9S{iNK%nJzkV%dIU^9d45_P|yA z9lv|fPA5AJYu?YTI+8IbJ7i<4J&@s@f7Q{Snh~qbWmP}gyyy(#$J$=my)W=w+f5I; zu*{WWD0v%t+!i4_u4(hGsqtC7mAX}hGPge<2J#-mXClp2ulU8=RDM#f9!&GXlGuTF zxBYOyCQ^c3N)i%>VTkhIc8(L4Oe&Z<(W&&-(2FlFj>$IL-5Y z$k{Pw%1}wa_04$09)J0Zqr34oX#!9sp|=vIXI+WTM;fTg8Aaxglo!dC3!XyXrm3-` zt4x!y`strPpN}<7_7KXg>$-B7N|1HpdXR#VZRac59-ls;7TD=&q*G1yJdQA~+_=@J zBuau4N zwRO|oGpSkR3$udg^|fRiQ{wZ^9_;uz;ff-?I@t>eY~+|2^bBu*D`qNCbjhik2pNL%*; z&*VY)l?CPQ8o)t&F2?dFNc)d7ad}4I;b|NkiNY478EMLe%P+&;Ti*?3eyK$1PH9~6*So_wbMuZwUBlf|s(aT01A6hzY4lOh&HayCty z{M!ki=i%V6YSvt~oYG5h&Ep3|q~Wz^nw3q5QNuW;Cs*kme7^u^1( zTCnX)gDpBv@QM71x~cfr6Ye$gEfXC*0rLT={9yd+?@W`Kt~DXIG`l(cO$|?uBQL|2 zCeN#1Nh&w?uL3HI9qA*nZ=Y(wYL7G*?Y?UuitDi@j0uOpcFhhKNsypXV2EIeKXra> zOzYbh5rgiSnNi^52Uz$rb0tve*XS=v*49J9erW_>C)8usd6N|>O zhIS^_r@dC>UlzZ?6lAhKz<-CUgbAmoD&)N~t7)m1Ob?h(vfKk<*K@M=q3tD@MQC~N zjev#i*hzc^Tc#s&@15Tbj*Tw`xigz4J(qSpd^_Bqd7a;GpVn@i8~qe_&bZ%9?m^Qm zD!XxyRkv7;J(c0bsujB-D8X-dm2C1g)z9*5sk}w@vY;7B>;b$5O$za^zzF%RC-c&@ zaY(MHK==aZTkNE81Cb^(ctf~3xuu@flURD_C#3vqwOX7PI(WW3A>9nz$#G>q{2*IC zX?p;VQNDTq`Q^>{2U=U>8XBDJCT8-VLS#<6pLhj&+oK836x9R@%F5^{8j9)OnPz2V z+&{0H>#NVPh`(5?h|Xnn0awgK)(%Pi97*=gu#pv#d;U%&wix5FHw?mf0jDJ!9gSPu z7OOl!w|UvfQ@Ort?2i)IMD2;T@cyQx!8CtBdcVm^{jrQe%B^Q|73=OZgLb-{nj#4b zGb13iItKz>70T^TFOx99E>7GMI$D^?Wz-SgYptWhyeQk=>Nvwe$YnaVvT6K=I^Jf( z4xsch-c;h2e^eVw7jyT7?)1EImp;iAtpIXshNEsiOoAO~=Z=-DQLa@A4b9ctv;#FN zt~GK);&AJ{OnxwL6Cd?}vVb@Q#JsdHA#PkCS|6lL&_4ck-4vKA5CcCxs)(P%_Y2E> zmc3Ujj3Y6Ctr@NT;dNqKm;kd?+1z*YJYdtH!IP7@W`&b358zC>HEUz2iIQ%{k0dVK z69+ttrA&txHGWH(Iw^Rz3_*=X)4_-ZRIKd6 zMoq76GbHndqh3GNl@|?YW5k6?gvd!+wECmh%`}4zjna5*YG&%~##jB4)10SG#vMrm zxA@BTgpU`Z>>BRO6A3R;>K(HdZ+KGg-3w!cNY=JWsK8+mW0l7%B`smn1nUfC?^*7w zGeYfmHcfp;!Yj!uyY(WKvpyki)Xr{MMtvkml3yOgCP}~AQ9r9^)y;X3)n;~$KYC!H zQ(v4sVx|(d&s61A-mem1q`-U6bHe~Nzsy_Rc(i=X$z1taKvfTII=u3gPq%oJqm^VkJUEc#UMZ+LXvkr7eS{|E3Yk zZjELIVGct(iFKKeQ`#BM3XWRJP_|h~ZwoXhD9K(yfH;J-x>cY_@QX0A&5RS1SUpIX zz|x4};r|6gP*Elnp-_mP1n8z?MSaY|-WDPuTSPX;;YtW>Ef^UD7+bvQ7Q)ot5WJHI zydp5ykA+_(5UdZ>EwO{tKMp;)p~0T2DUy=Dsjx!su<^S=jdg{z<(@MTr#S(b3i9I9 zbGY{fGl4L!=cdW9FGI3d{WC?wZ~EKvJlA8j?X5Mla@bU7xNdTI62w}-DATWw9d4ao zrSh9m6v~3SXRA4-T%MN?H&uDnHPuh;1@KCwLkkvX`PgHu=<8 zDbBmTs?kA~ElKV&xBMaM`o0mAKCBYL(vj8Qs+eX!OYPzJG4p-ui0;2qLSP0_VS?F) zpCWUO4i_J8rIieTZo8xDtcJO#lW!7}E-CS!QifjdO)H4Ce?SDWz6?7$;{W=ipps9~ z;}76YuFuOFAd`5HklbX{PzWYM56#xcJ?&6aIFgFPpu(LHDg0z23?``^afx<{T+`J} z2*cIyKh59+w}3rDMmbNIS8NRQcl&p+K4AMQxS!DEOlqD6LcO7I#hClJ-LTF+QH|%) z9R`0DMd1?T^Z+fJYF6fQbp!P&5KO#JJMvNfWVWp3vDR|jqv-}2Z`I_yeizhMn{6LqA2LpS8%5xkl}=7S;0Q>GHl8VlUR7n zb~j+#YwaCFq(K~$H^*5snXUg#wIh?FL@OAMMD6}5#^9aF)7GHiWvFv@x##|*3-#$1 zzduK@NtH1d{4z0PNKEhe7r6Vr9y^W4r}NlqX5|A7tDMQd+8_uB7JwxXU}$P|+pykk z9@{=B6iQk7ZT)FR%T}@>n_7Mm-=Wn?5kWV8Qsj7shsa)AZOC8;eW9z$DVpmCC_;15F!A4%39UBFv>rEeF6I^+`Ga^ z>LXjMz2Lv>WsCo7*vu5W+BRP~cyuM8C-a3O!yc z&%$Gct%|HcgD6jfhG`Nxv7S`~Wd#pCDJb$c3&z*6yvLkd&>2QAfz(eu%Np3#R;VwS zb%i8zgSD}}R}h$F7%sqb&Dsu&Qb1SpUFNb`LW9pQe{XUQ7hLnTJGx~`vOm)t2h4r{ z7{?&5Y?!S|t+wF&1nVo`GBe%Wa&CtNPwSB~!5vnsW-5_0AqE zd-Z$?@C!cGJ(O-?hALZQzgeIPcQIXD)fcbBfKgeAS!?7Vknbdl;7TE5_w_tCRpShU z+WKQVG;Fu}4UWB{@EUER+9krRF-%= z58#FKUXsMGZ*sI2ZHR9B-MXScLLcwg$KIBHBS1RxV<$SB)c)h%1%$4O`K;at+q10c zfXnmAS*|#v!3SMo&|cL5w4a16a=pkj$%Erv{#@sV%vOmD`6{&*dYj)2fuI+PGJZsn zKa)+)JrN zy)qULyhaZdzLha!Iv3z!jkqNWfvB*j4%cUB|0o;=jlEP&8Uo*^Y=n5^3tPI*HQF{x z^B^W+VF%YwHfLnA%odbi3kp0q<+RQcUZ?cO!_W`Bmr3J~g@TzBD4+|VIA12X91=z5 zFaQVcMfmi;e&73UaTVkEs6M z{-+2exoeTg?~~y=ho@c38b? zP33qHXr9f$M^$^K*Er2q$CpS$9~NeZ=D12E#-%})h$msZJ5!xj@5~J?8rM4|`W{!u z^)+6H!T*thFlK#mEeN4bfBbC5sc#iPw#TJZ+nxL_Bjz=oC7GlSlgue_IM|> zO~|EM#N`!aw>C=xTVhrWvYcBd!F%xG#gbbNY6BLAQ!(W%+2{`grYs&JhR@HL;Qi&1 zs?Ju*OZAjGeR0n(RC>|#vAh==qQZqq(R{3%CT-^faufttLp;kIJ%CNHR&K1L07;Rt zYU1< zVQpbrLGHmPD!$N=g%yXTTO^q(gvcXre$*TGDc2dNy#WHfOH{5S*UV4AIo@3`E2L|Y z=(jn%5GmbUD%XW;sBv0-f>>w}90l)U1;@AfikTag`#u5v-?`OYb_>To`Q71c9}n+0 zdJ%sdD4WnuUH=C0{Yb0v;+Yp03+7~<8@q#5htJ}nz{bdoVn9cJtx#USGCLYb2wqc> z6VlM+!4P^AW0sQwGSeQH?t5;1@n5?Hb4T~|^%2&~CYfr}CmN$abqn(HG5n7fz$VDV zt_@EqVdbzQRtOh~SVsk&pC#g>$Q?f0px{Od^76zwuBU~aYKR=O)c+|FX}qYA>j4%W z8BeuiFFi_<#~;(=zLojqU@B`5``riZ$_}1e z#p`JnyURL5aw%{#Fx+u{{r2i#IJ+R3l!q1W)Q9`W3?1a;PU$t1^{@%Jf90(6M%{fn zVV5Q!rN{Xqvfj9U^N1;DV8Km!ll1j7ES@Pl)o!v}Bb>x*5>IfrkgU1ITQaC(S=oy> z0D2-3W+k23r=CtjM4-1*P;_KLeckDJ{Y*fc~*_dO3?L8r!l&Xi1d&yOrr3Nx`ia`RVyO9b2z6w(c55piU@D8 ztFs;KYqW8-A5Y)ppm&w1x_9DWu~^eCuwBSn{l4&`*UDK$z?v$@U9lpEx1qpc|NA`N zbgi~A#$YPBQ)42WjDrZGhr}lnd@+i;x(+dngMrgCUK&=*{QAe7onJmJI`7fdCkQVN zYzL==fztBEnX`>6CUL)C#UeD_?z~#V7e1oRD_Ksixc0n@!_qm>@nmGCn-&$k9Aq+i zO=1U0Eu7k-=*`J^Ug_OL!L@Aa_$Tc5FPc+X`bh=xD4EPG8`D;u;0v(|5@vm9{ffpS zwWkf;%J{df?V03sdf$jq&vg_pYKe-t$6AFRGkl5K;v7 zn%B9m*Q^&?``O~|Bkg3hiM5P<3U_1T)5q&}Ge$)ry{hb~74N?`Z)ef~f-96c|gWw~lVU2rsyzzJ@J!RV-Bu?H^`2s(Xu0JHs#mBN3wdL$$Iq6 z{VL3GI@NL~uPni9?2f@qD0jA#`h#Be- zMNdcjUPm+Z&2YXT3<*&o+6fj5bTVv1Na=y|=|+j=g3-VdnVj0~H4MsErHh5QS)@UR ziTTEr>j^^VQ+rVLvnK1?Y{vzzo;7(qvJxKeD=Meoof~3N=&|e^FCns_?f&<8(|q>P z*(iUPX$c%e3c_gZRYTwz362=!!vS~TZD;c_L zF!bdfgHNz+BOq-iDBOvlBtOaE2T8Yu3kAB{<{W+9^f2EWy%I1NqapB>d-)sJ#=${U zH!+5);5i7i^stBWP@bC7^W!^IXT=mt(mi|5_XC@f>w3E$<^M8RFcaIE)2o1){j9t1 zv}t{YMAtoIWPhtydJNU0UldF>BB@#m&C5t7Sc^{1V7Cux3)ubegqD4(9~ z7T@!I!Sm@jT{WC+?b*dqw&5)loVn+X?Uv%WRfI2Pc{>T6ikvn%2)%ug#mxEXWxNg#!P{*NtAF05zcJ z{*kWn6tfEUv|&-uM@hq`Eq!t3n%(m8M(6Q$A|8HiG!{8Yf{OwiCNUceU|6KkK-w9g zYi&+p)xIugYd}R&FKp_yQ9kOp=Pv1!Iiy1B{2KOy=_%C!Tl3tTk`6 z7cnHgl^fi3bFMU`$clD78YSu9TDt;6w@o?$Ol4j)fj+gYtSr^`@RhJtwX*uNkL`CP zx121jTP(FbHO3dTRxdk%ve9sSa@ErG6~x{##w1o(0QphOf(3_-S8v35Szh*f1xX0} z=FBt-jGoPd_S|{!phCquJ!k^OQC#)KA0aDwY?-Mf?hBB3*t7!akC4;*hu^AP{QdH} zJyR5IcGtai0Sm%)OQ^~@FZg>SWw=qdf~7~#@FqM;Jl>bUJuqMN*eeqW4*BNUAaK4W3^~lCTvsoh<6Jv7K|B{ zIBX@f7Z2nu7WX;?y%degl2jx%h?%aSp3hzUs|2847(d5D0~)Uj-}TmP-CqXt6R~93 zwD=hIw>sEJ@=sgl=;o1$V74sBS0U5IEJ4#-0=xppJtuTd({B5db|VIoegJc=oPFem zOS)3lVq*{`?{%Ym)HxssawW-Ysp$MbR(7m?-ShOYXI4lUdxjzTJGsENoB=uzC%HMq z=~LZ`_e@#r-3tswYg)E-6hM#`y|EuG`44x8NKI)$1s2M40K0aKxKPn6~ zE``UiBlF1e5q#(6Go1-r?S`ScQQodhJGZ)bo!!v8teEJg#8B@tY1b8mpJMd0s~wU( zt?eFTXLK=|`Dn~sVufxVAHwtL(akJO<>Gg0)D~0zr?}?12Ar;I=j>u~$c-(gf-LJK&xwZyuVxWBlRCQIGplx| z(*<`G9z@uhe)^b+g_tr+ER^CT6myC3-1so&NTLyMZ}FxkGJ+bQ6DdJn$cSr0nnoFg z4c)Q%N7Gdvk#AZPU4+#gbjeQ)67N5~mXm+Hy>Wh=z;+|+A^c0w7M(&AK^9J%_mVJG z2z=K_|6pC3@f&c3cibKb?GC8;ck)c~zL1WWUoPr*mR2&EstSr7H3=8%``iFX!_d9S zv=e<(MRK7ND>K5dN^TH4uG0M;$lLx1*AIDz>q@5*#s(swIpAaNN=?>8<8J^kq4G%6 zn-pF}$B)|{&ZsFPUlm6h{Gh(Vp)uStR@8y?xd^fg@RRYm>(GN#3{8 zwru<>m6k{MP!CMcSWeDN>t3AHS10L<>$TOeo=3|+oj|ZFVC^(zLqfSV#ZMoC-7}Xa z01oKynh&;6R`GbtY&q1I*4A;$D3nHFBR@YPS2p-`Ckt2jWKl6nZ|wTQ^LGb0M=n!t zJh)`s%oLg)-#+`O2FT?P_3~b50o2?uWbpzGP5?k9KcyZ8PXjktX+uarSw)noIi?Cg;5H zsf1{76;Er1h2M42#!IA)-3Js-#eQ?9%p~_utXDNZl|9YNWw>5c$9uCKC=^p2zefC5MxZb zvXftt^JC>O^Wn1|tLp=St)u+&fP&6DPh5JHRGhmPYz*6sZ zTqGFvv6w3xYwaqcbh)g+yBWPvp`yrE^`(hei|cs%Q%5Ap^(dkaqua&-6sOu8W1{3A zVW7G3|Ms(tT@nK1PP^Hntc=S{r-tt~;zNhc`OVJdGv$G7eeWgwQI;QD&x~}x#Vt?c z4hw1?-Pw27D~pf}TKHP`?02}PQ-@?^wyNS`CCi31rzX-+mRW0;-be%}0YAf@esQK# zxttFm)2g}5Wzk&Ie?I}`!M+jd{c=G1<#Fp04Rtf0=IdkSyA$U+1xrA3S(!>Q?@vbi zug2$w?R8unv7}=zfz9S&)aw*s-Q_E)d*uMhBNKO*u$ z0knHLy&CwnWKWOlxf0L5+|G}{-k;OKe?4e+6zEA!ynX-I`Z`?aE$=geHw6E2OyFN$ zdx;hp6oXXfpBLyK76$xI=(#efYqxm#U*7beuJz#q3pH@<@qZw!^xq}@?~?x4N`K)` z{=4!2|MsPw3>#=mD(-3d8l%5{>7GYEhx13$B;DDzPG8_h?iPR3u%-b~klua7=Ivjc zb~PAY!(s6RV*A9VMja~QHpTx`p4}6F)>Qq6xDGOwOrT<6 z_Fi&T;)k=|zvmL$=jp7@c$$f z`Pg$pd&kB|uK+^0Ic4gvIG)FCXMU$An1J6eHshQe`5AkN6tBg`G~K&`3srJ3 zM5NoCrR!oX#`YN%`Si+K!B|iH!_98pug+bv7|1SfJX|erPZPC#Z1RNAxs{EDg=NtO z_aSmyQ%ft-35$gV76qow*S`H5W%e(50SIe7x#t1*Xm*n4-#+$#`G?ts^W4I<)h6P{ zd;GZGTsfaBAt6|8Pe|JaNUc@OL$F^zi>ud2{)nOf@zOtknm-)B#QC~prFfrt0Z!7U zD8h?+hl>67j_m2;G{RfcNTfJf?}%MQF^f+(U-8yt{Eu}}eVk)u`b6$ti0(P1 z7@iXF?-YmbnkX{k{v9ehXoTj0=ue}|`I|=rP$}B|KbTYAOzmS&1BE0@%63SBcNCkB zv-zXt{cpL#pZwK;*arb9Bx?XJn=F?BVbgw)D6#vItM3k1q&Y8|h|HcdY{=LmbjVm( zp$HO=_uk-WB;l9$Brr4d0TMOdPXtGJ7;sMb68&5#8@ z;qv!bE5!5$goUF*>G>Ue(dcysSTxO$SX8m>eLE7ko^_pd!d3crHfFQWKc7WLzo zlBa)zFM$gsXe96x5y$0@z6vWY7QK?{PCzrW49NJnQUDkMXFn^_>OYD_{b@b^xTz04 zG5iaBvsqRmQ0f9?MvC3=31J8nv~D7&2W%35Wo*V5{PdBoiwgusChRm*QrvkG2@)`c zirz>t&>+s>XoWKGSBckeKJ+&aXm4>Ces%?nqA>Daww?89;lWnhmRCaWWnk$B0Z>=C zcQGA~81lrb1{xxDfv{hJ-wA5_`DJePIZyg$UmWZ(;|nm!a$j(U86lUVj3sWo9QGP= zayQ-3j0RzhIX7N&*?Y5=AyZue6o$%&UTSKTA<#j-yQN_k@>5Fr^i{pwT>2TzcBI4Zc;w#6c{c>;Dqkic>L_P#w5~4aXc@KkA`^UjrB|=(7 z@xW0r{B$l&BK=E%x)2|hva19KR5?x;MitU(t0)9WsORkuPxk*>r2Nw=nvwe0A}~Up z;E z&t)z)P6-If_7v7L4Y>k~9OdexWc zzJdNR9H4eZkH-_jGv&HiP4LPLoE2-a`O>1*43O|83^VSlj*nf=$`uitDm3G(U<-ay|T&yIs1ighjHGo>J;JE6dsTNa|Ug zRE&Dxkd0958-H>`SL(y4%T(xFqgas9=v(#_XUGg=e%;%Gg2P_m?AK6o{1vX>sycC2 zck9sTCNn_J-;#}G&8;8^5;LlqN`p#+k+Qr4f9B zQwNp55f@H?R11k)!Vm5b_P|-E}!j?Ests+&4x5>r3dm_h);xj z7k>~hPh<<9Z)R#Pp0zZSuZ}%cS&>W$McYWvql+S6_3}%j7PdYGB6hf@bOeLr( zUTf!Et7lUR_jEgSp6HH2SM6SxK;jrb3=pkMv1=2BrV?nw=SHO<9N9f%gpC9owMvZP zX_7=Sl~fhmJ^>TXxq7+riIL@8?kz#TdPahUk0PXQjI^*Ic@vm0@|C9<&3KoDH@wAY z(otBW#rvqG7bcM$cbWE6Vyqus@f$G#I@ZIJV`r2Z%2G{tACTU+&7-Nqj+KZt&Ezw` zv<4Z1Xu%gGo+9RV0(EEfrIC>gV;s0Mq_||<8`u!Bvb?Yz^?ESebT{|cQJ@Qp=mM?= zlzNis0SgWpsn}lpSzJvRF^#a|3)@L3?xiU%?~{Q(SslD9ae9&cU8##YOgel`m}2|F2tMRaLulNB5;S(-B-kI`-ac4`-Y$8nlMsH z{7ijFEh!W?LX{lk0&T7Mb1g=7P@C3QH%8qw+`JNn)pV!8D6CCii!T9vTfC3On9a4& zHkPbAZz$TrNg*A@zddpeW#)}oZLt$K;0w+5tL%zZ)vZywxA+h>eH?gBwD@`c=A(d> z9^qfkMrTUoK;YLSc2@kGcy1j2J!2S;vB@GMfr(GRi0+{o#GIc)K-poA`L!}*=NN0w zW5P!wi93 zBr+GG@$UJXsL$V4u_sI2%`LWKbTCo+7){2hk<>XheKAsN_u|3U_S9-RTe_)PbL#9`i(kNAew?X zL6?^rhjI4eb#q16soS4{oaXYR55d2YCPM)CpXkeCqt%BcSx-- z6a~{PMv9hIn`=lbrO4#D)~Y40sGqu)>o_Iu&;#!{`#7yYd2F;nmpGU4qn=aN&WhpN z^wr7%Ud`NRf11Vd&$(1c=Y^(95msI$ci|?|j3yHgKYcM~s~oC|J`qukalfro^Fii9 zCCkL}xCE+jQzt_WFY|SEH8yXX0zkO)!}&eXF^5)MnXw>*wzRa=_=F|D%y^wnRD)oZmL^d~?~PVlj0F1u1y|=mJOPn{O{b-o zMk4vm+xc$WaT~I#R876HrO{s>!|}o1;QgkIMONx}yS=-!0n0BJxu8sCn33z#%YdBp zI*EkPI>=72MOTz1m&N9AS1T-59nOq8pHA>yfsKh?8}%!868fq%LV2@23jv=ybl;p< z!W8KaIg~j`w&WzH{W8`NWYa0Na+mrMw=bKtZ2S1CXN`HeX&7jR^?kh8zHB@kJXaQG z;?!QCRRQ)+Wmy?0GgeXuFI?BqC=W9aU|v_sW42_{3D4I8L+04Wr8Sw3<8S`5Q26~q zA`xTAF#%Hr30&$Gw)xw3zQZl%uS#XPG9%PEdQ*d^C;XHwsXdA1r;c}_5Jd}NU!?_^ zT$McCvf?d*M`h1*-rmfQ-pg)`fE8CS?n-8XX2eI%U*G>SXtg#`Man%iXHQ```po%H-A*?S$pirrvipj)elus{)`|UH3+pk(l@uzTdJM9yjuR)pMX%R?f;Dt;V$K zt(K)(bGTi+n(Jln4SISN>NX3oV8$%bdE2`d*7J%V)fn`K)GAoWd)3tH?^i3tbV*%z zS^C}{U-UO6pC7}@d+iUjNpI2#`EJN%t1{p)881u4Pj{)zcUW4_FUQ)>cL%oc2^xy$ zY6}(;gqO0+Ic;GMS64zbO$OU%9+b70>;*0B-g=bkdY|l9@2a10Bb%E~JTIOZpVoNy zXm>f8ikSw8w}rG;L`;V?DYqx%qqq46rUaRZoNldyQ1nXSFH6q_6`%L^{y`US zUe;N2al1_u_%M3*jzs_sI^%9ZvL9)&Q@z+=`&LWIs>N=z6gv$)PUKh7rpH*1N?mf% z(di%DPa@x&i==M78az^RNTT>voh$MoQ-pd#$-R=`Wi~GNtmPn}sPU;b;`l{Bs%hC; z9UjXc+XLXk+rU?vEVOfjlAhTDDRu67d3kRc9DAHSR^M`|RVdTSXqNqwukc>&!y7F# zVcZDR6C4Id$6W8TQ;oMlP9CdsOv9gsGeDB~I`KNABYU5iC3#m$`4V0Rg=jL&r3VvUfT)&oG{zOZrO;XR9xMCCNzsnlO0H(UuKokbKuSA)m|*E}StgFd1B)7g`r z?yBcYsx}f;%^Y@WI@OSez(bv(+O0ZOC)L(45u(PcO>e}=Bx zK;cJSdkF^a^46dJdkbTgUqOP$kb86WgPj*f#?aUuw z5$ykQh~nR~xVPDBoj>-MHg`xbyDUN~bXKED(_ZDl&fBHZ#H4EMSZ zUKO&%;><13JLo)fpYORsIB0#KnY>2pxR=m16nv7cjlHe!==DCdojCfJZ~g41Sv+gn zOa!IHEhqX)g6>tTx??9k4~1~hPmnr!joooBA%CC~HegyqP?)W#rTNRqRY%uzA|j^f zF*4dCTC4DM>DkFr`-2xi${=yo{hPrP2LMB(Nzo?>*wx9x8Y5^|SWD0v2EJKbu>sOHTF0&`v8YX+`5ms<`7120lU@mn zZ+5-f62>vxz4Fyr@%7C~fU|lH3PK@ds~WsC)+rA^Tcu#{lH-0@3}2#Z5k2+jc_VFP4Sq9C{0+V{_#k%S=@^LobKuO|T@+MT8c;f>{yC#*H1-?r${7xMh)fJ9GFs z>>60h=kp#q%XwpL_HngT(%B(&Ee9K zCiumIy=z=x=#BVm zL4wJj?o0g;u!Xl+OnS@3t+F{PY7GiHfir%4J(biCjFNL%-43d;h0<@ zKfjldyX4QFU*rXNilya`_b=c9I(qL$xYX63dcS`SB;vULNZ$&K!+??{MGF1chVYjh zScwfhg}byxz-M(D@ZvE7jFYr~4kkao`v+FAicSUtSUyFP;Kf(m)ZAr$`uyFO-}6B^ zM#V2Sybo3&`3~ihjx!$%Y}kP0q0-pAaPez*9=RUx7P#llT+cd-5)H@I8K1kM(i!y-&e9SAFU0h>78F&(_l1Y;ipMmFH( zh+lW?#yh{q)5t{SsZ!SW5_s)uSzK(7GmAC$)0d~J%UqVInq)5$uNMYyE!W($x+bQz|ZVUQ5iQ2CgVYCFgWE7ogd< zKU(wrf3-l7G#oBfcEj(+)gjA!x+X=7*uVY8Xd*5C@Cg?!)e);k{jsz0!E6Ur%x3?Mba5clBwd++o6<9_A6f86=wJo7xWX79DvT6@Lk?6VHd zvrTA_+vd=qKguVccK4xOREOPq=pvNqCiUP_={NSz0>dtL!qBfOA{{QQdBb0N+u(xo z1Mv)IDVz@)ssraoTin}fcy2x>40|n-l)>aRX-VHRKFd*Xi$tNn-(1V^_>&tmJKpqk zqYnQzLSh0rw|_*Q4LP?cx=yS+&B#jQ3Eg;x@$HL@Oum0;<^;qctdfM#GYC zaYjql0wV5XF1D0OHYk_XZhdP|DW(P{=~m{TW2yYJ(!mMl*?5(f}vuWAq z@w_cHRt`(z%FF6j#*~jQFI(E=?~$c)YCm6YFwMy^t(SUek$yT$*;`@WNidTUTb(ag zalFUdkhfF`bX57QoiU{$zc-XU^t}Y!*Hed}5Z>JnsN8UvJt&?y>UZ`^v^?q@%rPlm z-S13YZw_I3m|!>W{{5rE%vaxsdi9BF9I8EPIlp3yg}p$Ri)%5?LvX#X+PP)?>yi2n z&GoI-8_A4e#8KKrq4H^=GEet9>+7_ptqXL`tHYXQRT%ozx-8WARSFW=Qga;mg=$2^ zM+;)#ul~?tvAs^`p+I8cpC>bZ!rI5M@BW#Q>BtB2}i|^itd<+ZLHk5&vB)uD1BekSp)Kof}33 z^DDkM5h5&f(fOHNxR^tJD^_4jDHI_X+pwW=*q(fLv#e8|g`7(=*TB!hn7yBGQ9zXZnJ7pafHJZ}O!N#&*wZ`=~6GTHBZl(EjkxM`EUT=R) z_$IlcjlVp+1ryU?4m`}P~1 zTrR1{7s-P#9AnuK_3x?;zg6wq?P=$s&+a=j4r87@6^hh;>H=F1)+U>ED;YM?{+)Sa zsPt7wK5n77V(paSPYjwqyI;TK-JM!eGJ8&(ds8RNG%!O|CR<3TNIlKhiKK`yq!jcx z>jYOAUcIpz|At1-_l(<0*W7c}^BvI;VIPv1m zo1i^~6Ns{fE{BSZ`Fe)yCMBWqhXVwREN$oA!Dv%z({!Z|;p8tg`UXKZwx!~kgU1JY zBQ)b)AV)=#)e#nneHUR+jN;asRI$>xtfP$=gJ0_M8CGn@NnkM{m22}UV;l)wRJ0l5 zSf!|T%F>DYgPz8{&$ zLS0qR<;;84^@;jj7246}0}EOaoWO;X3y}#>z(Z891>&ue;R|@{iHza8FOPD_|p$vA0a9Sh<&E*atH(56Z`%JioEhDy&P z$K+oM@qWGeV-==fk`~(x2&6L%=Gqko)zF4kj!p(A*7qgO(fdvl^){w{i!{G9;^hdc zy&sIY=I@G5GdpMvOVy(zU4gs1OcE#k8n-d)?0!`f-@wFu=%$l(ehsBApBC1;QFpY^ z{jO%q!fI8+*c%fObiaVRu5=RzG@Z6}LLmYP@?_pDnWs<|%YxQg9vwRI!(J3`yX0VPk|`2WSgVr0Eh_VTH;T^w?AI?%j$UGBQ=K>SH2rHasZKad?!t8egt$r6 zZkp*laa>iX*dX(*=bd=Y;{{Zu5}Hf;)%vBL;@`QL!Un>#V&ZGvjdK#S3yFfSz#L`b^OuzJ(D9L*PU&@K^3#28UKg=Y8<50`dQ*k6B zTEDv!9}Df0G=5f9ZW5j%85m_@yEZ9cRdWJE%Ml}T_0~nL+Jo7GKVlG#YSo;C?4&Ej zhS7FHGB(t;1DLYr=Bu4P#i?_TMIAm3XvZ%X!THABADk#2^BgWzt4qGX8*@t)Eco>3 zDH1M@q9av9k8o{PKGkurs&$da+4o_^;Gfh?if?PpY>Mfjjwg@_TGBU#|IsEMZMI+` zbFkIxzHX*=)|q&iibGPri~};?zSsqoK9~)2!-_QYK^Dtz##B;#iiFjObHk^!QF4Sc zN!+tZ{bj;O)wZ!khRo&H{_K#c?I8^tu$`(XTqA}$6VNXU^hBrN&%BJStY{!pB-TQ2 zAzdTu*wy-JY`sQP2sF>ry6!xi*bIRSFDK7ThvxR+n-VbwKZ(WE{n$K3NRXQ@gwjzy z+J(t56U+wrDV)JBY!rjDxE!iZm?TjVK`4GsXWAN`UOyqC(Md;6UY=5$9Mr6bjBmaB zyMwEjdHCfNNxV*D1!50&?Qjui=xEh$%VK#oNQ^7Scqd=sN<9Ks(GGkGhvrm5`6Nh!f0J1R`~&~7Fp0$zeI75 z#xRtd2!b`6CtMoYQVDuHr18-XofiWnB+dTM9{1K2`zJ}3JOC*IiEixx-^12g7e4|Z zU5$$7T^)1MSWvT1v|tsP26y7Bw!jl$wY3H&6?hpEAl)ckq;tacDc&yVxi9`lvy;LV?HOC7m&VEp02E7VV z&*&+Fun^H!qMa|L;4YR-Co^PlESNz|F^JE~5GQE#zL3TdBmQSu2kkb%4{U2@)|DE5 zEjz+F^v4R@Z&8hR=p#~%TC_Zr13gv|GEJfYytCjsqOw^xKrD5>$J^*^iogW@4xmb{ zlAnYfLbyjQ=vzE!-P$p{hJ6&s6G!A3yA|y=Lh`K`Jxgt|Qix4M!)vL}JS3XhgOLO$ zV-O;U7c$O~<0ShrAr6;bwr}3{MoT z$~ph$j|i)X~VY zuY^?%J|LQvx!=BBVj81uh=o5Y)b>}8C`+LX*R*9dI1wP?-V@li1?N~V#GF~?mR>QH z#zS-ndBuj@+G2jC75A~D&^J9TqW{Mn;*Uj#NtR%M%$))PxN6=Hz!}ryDZc+k$4{fw z((SS2mdblGvQ5faQ#_Z%4&++ZN{#hST4b(nU^A?+fs2M|L;v`TNU|x;N}F+*go_AR z1P39!aArus*Ugh}i)^7Py5ox!o|HS*DWs8u4s%CEFPJ?3d|sIBopxU&`d2A0%i z*a9`%(KUSpV}V`EgJGuwJ9PSu<`HeW;f#a{EHY;U;a_{O7W+T$<7AzyrYHP(0?%DG zd^{de=SPqPHd*m(DXgW8yN%Tg!&$Gbi#AP`bjLz?E1V86XUqyz8OT~YyH#>-oUXUA z=G%jXzqk0hpv`hJVPtii!DpjDrv| zqDIGJ%n_MZFK5c3U^g_@N~>M{94?!D@JcQE@+cJ3=&|rd;m4Ry?HJ8@0NMa85j(pE z*;{{lI^=4K9hs>7&7QENY%aKQcPJ4o1dra#8fH?d= zb#=U?nL?qT_uJdKks0tL6LZ)_rT&H#Kvi_R<7wTpLw&h7PzyF&~oAP{Xo`7q33 zP??kOtF9W_q9$1TN4O(r<*s4q{Kc51)dFI`RY{jEg>dX9ORPd!JV~&kdc0c}oA76K zDfH!_eSq3c!S5j|5WUhN5~6j4$hSo!y{ve27zt3(ibKrkJpOnSyL?oj2+VTmj5)F| zbwz4s6KZ0W-g`PKclbPa>ky6pB$@oEztpZ$CK=XeEKPVE^FKP&1+kSw&q2slT6{NuYGy1$*34OFZC}^^P6%BGoc*5)Dm6b34YuS0di;m!gXWeNkKo zkRxR{>)0Fw@}hL~{nqegEb~+%_W)Z7_W&Lf{?i_m7eIZM0KMRM%+(x)?Hfu*2RA+S zGN#B&_TQ%#rdQf1I@Ew|jZgS9>#DxLMV<672Q)$$M2GcDi3&j6bjr?%7DD??iOc6m z3vQbkto`&}B`g707aj5qu;BA|xaUs~cLZ`ol7!`rg@q3b4n8=U#kbM|KH}FU?rq&!U9u#L8)`nq%w!GI!{F1}; z=WH&lel>^WDIU^*3*tiT&^Vl0MM+17)SiFnRC{43NEtP6G*qmf8)g2rZLSIM{QDZi z=jdjq9|on(XRZiLEpZ!nI!OuBw``;yIgfj{BRLdz5$3CK7JQZ_Ix|ohZhb${_Zh;t zcMQ`ge#*0gw8V$p03KfOIQ-JJFJh`yTR=pFulW1JYhT4n%OEG$GDH9U5jW9z`|qsq zQoqan+)YdB*%KZviYf7Bmz~OwyyF3CPhKm_ld$3AdyYB)XoDU+5agJmUM&=x%4#z3 zxlY`maogjRw7SDJuz^E#cyXWi5pWcHtgr=sY_020e5?lNy;U}yvp)L_eBmvuY06=H#3@lL{&yAE@+V=aeTmevJ{yK63$p;zkNWO0bTaRd^OxQXex84%4<(8`TICJFXMGb6K z3A7m(0MOY1-G<@Xx7Qg*b}uaS={>gHH}6$`_Pcono_i7|fPx#<5c!Mq)wK??g`Q|% zqwvB-WGhv4p7`{ZCVl}x_RS%|9GE_|7;uj*``TO+ z6XW5FjY{g@wTD5~=*B%k<%^x;#ds~v>GP9|Hw#&BrA6oj?kEI9BulVCU)_Guoexyu zS4$U?eJGTj9+p7|9y~EMleBAFBzfccyyf?SH4H+=?@;JYR=K2wSEDRY;N5hR;5t?& zKk5j+HeO^bSfsb|j!Q!s7m~82J`{k1H8NQcH@V{zwv_RsM zD{&W9r|sFgSYDHWvrJVL_v=at(o7;^$BU1g8$zN_K6lSpFAN!zy+ONyQoSq}G&w(w z7cg08aljMd-m@XEdMPkLT{Y&X^kT(k(Xi_@Tca;n)Jmb2Dd+wJ{@BB%>Y$>0(ER3H zKh*gNSEQZ+haRcv`egRg-)n+Rs%r+ESwxD=k@Fd|cMV zefGIY7+UylDdU>A_{~RVn-cm{Rw5iBGkE4F`?+b8p_<{Ks!x46W9SvB$eRE2Ew2Br z50)Qd{_HdKx7KM>OdG9#gPE^YQWndrs)%tPX#qnkH4qjbcR} z_0_0EwZG2X;=0&$2lHx^%mByPGI#DMafl+xE?9}Sq7#MvEqIzP+Y^9oO6cQtu6C~` zcmw(Sh~uqImz`C8>ubYB-Y-PAz?mk(v{pWynzG_unOuDnDAvrJ;(Yf&a8I1+7edLj zJ`8}r*$o)5(I1BQJht!}V<}vf>R9=-;b5;+kTRUCB%7%UGp^n?v+wpHjIq3qSK~qQ zkxExqR!8&>y(h03`TTw5=Qz{lCk@M#gX&tTu7C>(`@JeB&%W(Sm07jkk|f^?NWl%&;5g4%-zhJOZQW8#O`Md2d;UIXAuE=Mv+a<4|7N#95w z1Q|-3Q!%v%kJOE^{S89@Q~u`NYY#qj=bVotpdlqljsy-B{4f9Zp&D9s${Sp!4;k=Pc`8Mud3+m7Y2G^9JvcdI9B>|cc9_IpW|L&M*g$)?oJp%_CDESfv1LW@?r}Ve` z%(pDiD53&{+kPuG_!w~jm&(0kTzAZZ;h%lG9Cy}v!uzz3098xrHh}IDTNxoh=p5-! zAw^SlABA3OSK;6;_2BCRgdMvsc7hoGx_~fxLhA1l0ET}wz#6wJ={(l`1SvpKIm#R4 zch19kJI}hD9W{n79bh^M7|H#Ak61L`lA3_&V1#wIfobfv0a5FUSe5wS#a)8Ias?{A z2yR$(Rh2`6fw!n6O#8yzc!Tt8i>;vd)6b5{9mT z&sg-i$BF>lGItJfZFswk%#ZY@UA<5%+^hDNE}|IzrIL}RIqLzU zRsjb%_GWh}-dhqETX)hBzya&bR<-36L=!q~!}jqd+t{K6hapG3ZA@xmhaF`f@6xN6 zxlccNxbh5F!a>6O%9^XpXcq?zf4{aU9jl(f;{L$SVmrJ$d1+%LtlMkH@W>k{X6Tt) zf;AZ1CS%h^&7oX4mC`kfXbf1}rohAQVg=)+CJOzDu=#p$Ld*P66}kHpf}ZC-&rC1E z&1~~IYn55(&>`DccNs|ugHIVI=*jGmAIs5V)56h0@(Ya^T6n0WCf13$y8MM>F*`&- z)_?u#z3ScT%E8AdH1xj;6`X|JDaG6E4|MtEremi< z7WDa#$TRT)2Zsf@kiOG&fl%U8c1ukh>63Ctdi1Zse3Wm!O#rJ0-5|e zvoOggx1mBRPW-M=@rM^0_FL+2M$1h9j);FXCK}+#H@#G*e8yeM{KF|e@E8Pr$10B< z;eSSs1$gUB<(yY{I;~sQF|YyZH@`^hF#j|1fZJ&Jb&UBgpZq6O_}@~OeRwdB@}H3d zu}S)g+BV@|h4*hu>bRxWe1>LoXBPBFC;z8rF3e%KKQBy68a-Zc@^@VG-?o>aNAoKE enf=+G*0~pwQ9};C^kA(lJAqbb~O! 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 z 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 K`}(|f`~MpUoHe!p literal 0 HcmV?d00001 diff --git a/docs/user/security/images/tutorial-secure-access-example-1-user.png b/docs/user/security/images/tutorial-secure-access-example-1-user.png new file mode 100644 index 0000000000000000000000000000000000000000..8df26cf28ef1624d9d23b37c696a2b13df851162 GIT binary patch literal 219096 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⋘we$7sc9eHy}Je=$N}`~jd-|{17xj4iMaIbq%OS{ToDwC*m=UFr#Zo7d<7Kt zt^AYe5v?XHhg(_(2nNZ$p@`w(;X`KL?8MJfS4d0w6yqo(DIhCzxw*MYIS8*z-t3%W zxFVSCbxfDlAQh5LK5KOR)lELnig5tPBZl26LJ=bIsielP6!J>Ig*-zgWajIE)hZFa zahW!NZh0einsY)CRgO?-SI7$*NXBt`FNv1~A{U@H?dFLU?Mn8DA|-I0Gfn!CA>eig z%AWw@?7I=$PnsWM_{slWDjrVmIHt=cqY6ZcPKU9k?G9DMGPwh!wpXJN_UHtHKN_BK zCfj>xfV6yvEHv}fN7>KS&7nk zw#2QI-@#_vi32ptKm<;3PV+`6j$a3wMdRU59kgq!5Q%{KZe#e~ z+5@aYFxu?_Y{8BSfHD*JFqWs^QfDXDNRp^+pBHF$t^*i&XmBtY4~iGB#gvdr*-TNN z&aU(}xbF;2Nj595=t;=%mS_uRAk8sRaDP9i?i`)FWWBGJ%Prjn8cR7fs1b(1s%!dJ zRadK3J$KL)0HD+(4!JJHu|PV~u6lCy9v3A_?`u)5Cg3I@y-fG{rh(8Qc8QU+#O8_O zormgxXgeLpbT%ehX+TPDM`N?@Wd``JW8OndfTh(?nL!=8b- z;<0V#GmDFPvuUw*J3*wy&8A6mQ?BCtd?~9T7j+|bO-&d8-n14X7C z^wV+YuH9|rzukA~cAu8gl`~f-X@L|8rw!<`RYk?c z6N6loZEZ!xMc*Lod1zJ9Z5^f|ZdlqZU6-D=jyhLJN{~FsN_(=j3%-pK(t%G;5-*1HNOtaFxc+C?Q8DalbmcFe(_*e%Js5)Z|!mi49{r-w^{g_ zG8fU&4o8pNAw{5X6;6hma}`%sifxQyRT)4N09~!(>}=L89RNRie0ebNa&Y&SJL+2= z-B%g@#vMKPF>coO&Dj>J1%Kt;E94O6UBQid5(1)rl_C&O0=9wRoj~vC=n~x?9-dIO zp8f7)6uC1bc!q>}QnCl-Nz|&}x868~BS|Au?;gL!A!FyvP#G5_G-#uK zCSq(kS?+Fhvheir41ViZS7~dE#MGrE^8o)IT)6HofW4C$g`GJ5to|hjT*cu#Do{0n9oc69*KQpj;kve>Kq1V6jXN72$~5RM z5Uynp8RZn{GkcXv?9A!xAuW3@5^V1?8O;t587x1om1nedd9iTn_AYC+CuAtg#A9cU zk0e-3T~<=paJRxzVebUl* z<1TZ2xZ87HUsulCI>?FxvBG9^#1pmm*=k8H0>|N zEG;cr%6Yschdf&x;d^tZ=H`C({(i5_#@z!6oGWHyGTCgzFE{1x0_;&_ibu$@|5A#~ z!D`6MF&@~jZ#-7rYwlZjU0w4+c&>T&X?h0i{Cu$QFXuA)$;A(eJ_dZ;ukFKKiRqU+ z4Lj@h$m7Ojr-J+gbc*YrBZAf#IXT3;&TiR0$?9DFMw#!>nD3r7Rfk|mng5V`8-?B{ zr>xTV*|ZsR$zIuc-K{Z)ti{554iCqX zG6hT9-NWSdR18*Ee%H(VFeIs+lGS|B!dGLWldtrG*#fygU&*JSQVN@ua5Nh2zMSFP zQCvNBSz^Y+xq*O{;qF@r5$iu iq9@HIt%7$A=jf@{jU@$rEE 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 literal 0 HcmV?d00001 diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index 6a5c4a83aa3ad..71c5bd268a67d 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -47,4 +47,3 @@ include::authorization/kibana-privileges.asciidoc[] include::api-keys/index.asciidoc[] include::encryption-keys/index.asciidoc[] include::role-mappings/index.asciidoc[] -include::rbac_tutorial.asciidoc[] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc deleted file mode 100644 index 6324539c3c10a..0000000000000 --- a/docs/user/security/rbac_tutorial.asciidoc +++ /dev/null @@ -1,105 +0,0 @@ -[[space-rbac-tutorial]] -=== Tutorial: Use role-based access control to customize Kibana spaces - -With role-based access control (RBAC), you can provide users access to data, tools, -and Kibana spaces. In this tutorial, you will learn how to configure roles -that provide the right users with the right access to the data, tools, and -Kibana spaces. - -[float] -==== Scenario - -Our user is a web developer working on a bank's -online mortgage service. The web developer has these -three requirements: - -* Have access to the data for that service -* Build visualizations and dashboards -* Monitor the performance of the system - -You'll provide the web developer with the access and privileges to get the job done. - -[float] -==== Prerequisites - -To complete this tutorial, you'll need the following: - -* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. -* **A space**: In this tutorial, use `Dev Mortgage` as the space -name. See <> for -details on creating a space. -* **Data**: You can use <> or -live data. In the following steps, Filebeat and Metricbeat data are used. - -[float] -==== Steps - -With the requirements in mind, here are the steps that you will work -through in this tutorial: - -* Create a role named `mortgage-developer` -* Give the role permission to access the data in the relevant indices -* Give the role permission to create visualizations and dashboards -* Create the web developer's user account with the proper roles - -[float] -==== Create a role - -Open the main menu, then click *Stack Management > Roles* -for an overview of your roles. This view provides actions -for you to create, edit, and delete roles. - -[role="screenshot"] -image::security/images/role-management.png["Role management"] - - -You can create as many roles as you like. Click *Create role* and -provide a name. Use `dev-mortgage` because this role is for a developer -working on the bank's mortgage application. - - -[float] -==== Give the role permission to access the data - -Access to data in indices is an index-level privilege, so in -*Index privileges*, add lines for the indices that contain the -data for this role. Two privileges are required: `read` and -`view_index_metadata`. All privileges are detailed in the -https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. - -In the screenshots, Filebeat and Metricbeat data is used, but you -should use the index patterns for your indices. - -[role="screenshot"] -image::security/images/role-index-privilege.png["Index privilege"] - -[float] -==== Give the role permissions to {kib} apps - -To enable users to create dashboards, visualizations, and saved searches, add {kib} privileges to the `dev-mortgage` role. - -. On the *{kib} privileges* window, select *Dev Mortgage* from the *Space* dropdown. - -. Click **Add space privilege**. - -. For *Dashboard*, *Visualize Library*, and *Discover*, click *All*. -+ -It is common to create saved searches in *Discover* while creating visualizations. -+ -[role="screenshot"] -image::security/images/role-space-visualization.png["Associate space"] - -[float] -==== Create the developer user account with the proper roles - -. Open the main menu, then click *Stack Management > Users*. -. Click **Create user**, then give the user the `dev-mortgage` -and `monitoring-user` roles, which are required for *Stack Monitoring* users. - -[role="screenshot"] -image::security/images/role-new-user.png["Developer user"] - -Finally, have the developer log in and access the Dev Mortgage space -and create a new visualization. - -NOTE: If the user is assigned to only one space, they will automatically enter that space on login. diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc new file mode 100644 index 0000000000000..63b83712e3e6e --- /dev/null +++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc @@ -0,0 +1,136 @@ +[[tutorial-secure-access-to-kibana]] +== Securing access to {kib} + + +{kib} is home to an ever-growing suite of powerful features, which help you get the most out of your data. Your data is important, and should be protected. {kib} allows you to secure access to your data and control how users are able to interact with your data. + +For example, some users might only need to view your stunning dashboards, while others might need to manage your fleet of Elastic agents and run machine learning jobs to detect anomalous behavior in your network. + +This guide introduces you to three of {kib}'s security features: spaces, roles, and users. By the end of this tutorial, you will learn how to manage these entities, and how you can leverage them to secure access to both {kib} and your data. + +[float] +=== Spaces + +Do you have multiple teams using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help. + +Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-get-started-ref}/overview.html[monitor application performance]. + +The assets you create in one space are isolated from other spaces, so when you enter a space, you only see the assets that belong to that space. + +Refer to the <> for more information. + +[float] +=== Roles + +Once your spaces are setup, the next step to securing access is to provision your roles. Roles are a collection of privileges that allow you to perform actions in {kib} and Elasticsearch. Roles are assigned to users, and to {ref}/built-in-users.html[system accounts] that power the Elastic Stack. + +You can create your own roles, or use any of the {ref}/built-in-roles.html[built-in roles]. Some built-in roles are intended for Elastic Stack components and should not be assigned to end users directly. + +One of the more useful built-in roles is `kibana_admin`. Assigning this role to your users will grant access to all of {kib}'s features. This includes the ability to manage Spaces. + +The built-in roles are great for getting started with the Elastic Stack, and for system administrators who do not need more restrictive access. With so many features, it’s not possible to ship more granular roles to accommodate everyone’s needs. This is where custom roles come in. + +As an administrator, you have the ability to create your own roles to describe exactly the kind of access your users should have. For example, you might create a `marketing_user` role, which you then assign to all users in your marketing department. This role would grant access to all of the necessary data and features for this team to be successful, without granting them access they don’t require. + + +[float] +=== Users + +Once your roles are setup, the next step to securing access is to create your users, and assign them one or more roles. {kib}'s user management allows you to provision accounts for each of your users. + +TIP: Want Single Sign-on? {kib} supports a wide range of SSO implementations, including SAML, OIDC, LDAP/AD, and Kerberos. <>. + + +[float] +[[tutorial-secure-kibana-dashboards-only]] +=== Example: Create a user with access only to dashboards + +Let’s work through an example together. Consider a marketing analyst who wants to monitor the effectiveness of their campaigns. They should be able to see their team’s dashboards, but not be allowed to view or manage anything else in {kib}. All of the team’s dashboards are located in the Marketing space. + +[float] +==== Create a space + +Create a Marketing space for your marketing analysts to use. + +. Open the main menu, and select **Stack Management**. +. Under **{kib}**, select **Spaces**. +. Click **Create a space**. +. Give this space a unique name. For example: `Marketing`. +. Click **Create space**. ++ +If you’ve followed the example above, you should end up with a space that looks like this: ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-space.png[Create space UI] + + +[float] +==== Create a role + +To effectively use dashboards, create a role that describes the privileges you want to grant. +In this example, a marketing analyst will need: + +* Access to **read** the data that powers the dashboards +* Access to **read** the dashboards within the `Marketing` space + +To create the role: + +. Open the main menu, and select **Stack Management**. +. Under **Security**, select **Roles**. +. Click **Create role**. +. Give this role a unique name. For example: `marketing_dashboards_role`. +. For this example, you want to store all marketing data in the `acme-marketing-*` set of indices. To grant this access, locate the **Index privileges** section and enter: +.. `acme-marketing-*` in the **Indices** field. +.. `read` and `view_index_metadata` in the **Privileges** field. ++ +TIP: You can add multiple patterns of indices, and grant different access levels to each. Click **Add index privilege** to grant additional access. +. To grant access to dashboards in the `Marketing` space, locate the {kib} section, and click **Add {kib} privilege**: +.. From the **Spaces** dropdown, select the `Marketing` space. +.. Expand the **Analytics** section, and select the **Read** privilege for **Dashboard**. +.. Click **Add Kibana privilege**. +. Click **Create role**. ++ +If you’ve followed the example above, you should end up with a role that looks like this: ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-role.png[Create role UI] + + +[float] +==== Create a user + +Now that you created a role, create a user account. + +. Navigate to *Stack Management*, and under *Security*, select *Users*. +. Click *Create user*. +. Give this user a descriptive username, and choose a secure password. +. Assign the *marketing_dashboards_role* that you previously created to this new user. +. Click *Create user*. + +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-user.png[Create user UI] + +[float] +==== Verify + +Verify that the user and role are working correctly. + +. Logout of {kib} if you are already logged in. +. In the login screen, enter the username and password for the account you created. ++ +You’re taken into the `Marketing` space, and the main navigation shows only the *Dashboard* application. ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-test.png[Verifying access to dashboards] + + +[float] +=== What's next? + +This guide is an introduction to {kib}'s security features. Check out these additional resources to learn more about authenticating and authorizing your users. + +* View the <> to learn more about single-sign on and other login features. + +* View the <> to learn more about authorizing access to {kib}'s features. + +Still have questions? Ask on our https://discuss.elastic.co/c/kibana[Kibana discuss forum] and a fellow community member or Elastic engineer will help out. diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc index a38bf699c1db8..bea13c1ef49b2 100644 --- a/docs/user/setup.asciidoc +++ b/docs/user/setup.asciidoc @@ -54,6 +54,8 @@ include::{kib-repo-dir}/setup/start-stop.asciidoc[] include::{kib-repo-dir}/setup/access.asciidoc[] +include::security/tutorials/how-to-secure-access-to-kibana.asciidoc[] + include::{kib-repo-dir}/setup/connect-to-elasticsearch.asciidoc[] include::{kib-repo-dir}/setup/upgrade.asciidoc[] From 128ed7f676f93dc4f812217dae5d5bbf56bff310 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 8 Apr 2021 12:39:15 -0500 Subject: [PATCH 126/131] skip flaky a11y test --- x-pack/test/accessibility/apps/spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 41926628c2377..dc0dfad2debf6 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('a11y test for manage spaces page', async () => { + // flaky + it.skip('a11y test for manage spaces page', async () => { await PageObjects.spaceSelector.clickManageSpaces(); await PageObjects.header.waitUntilLoadingHasFinished(); await toasts.dismissAllToasts(); From 91e1acd98dd5fee699764c3c0244d1d9a8d9f16c Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 8 Apr 2021 13:23:05 -0500 Subject: [PATCH 127/131] skip flaky test blocking snapshot promotion. #96515 --- .../security_solution_endpoint_api_int/apis/artifacts/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index e1edeb7808697..14e08992de9b4 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - describe('artifact download', () => { + // flaky https://github.com/elastic/kibana/issues/96515 + describe.skip('artifact download', () => { const esArchiverSnapshots = [ 'endpoint/artifacts/fleet_artifacts', 'endpoint/artifacts/api_feature', From 06e01c20e7a891afc766510358a9b660cb000b03 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 8 Apr 2021 14:29:51 -0400 Subject: [PATCH 128/131] [Uptime] Remove Location map from Uptime monitor details page (#96517) * remove LocationMap from Uptime Monitor details Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../translations/translations/ja-JP.json | 8 +- .../translations/translations/zh-CN.json | 8 +- x-pack/plugins/uptime/kibana.json | 23 +- .../uptime/public/components/monitor/index.ts | 1 - .../location_availability.test.tsx.snap | 234 -- .../location_availability.test.tsx | 122 +- .../location_availability.tsx | 65 +- .../location_availability/toggle_view_btn.tsx | 66 - .../use_selected_view.ts | 27 - .../__snapshots__/location_map.test.tsx.snap | 27 - .../location_missing.test.tsx.snap | 123 - .../embeddables/__mocks__/poly_layer_mock.ts | 192 -- .../location_map/embeddables/embedded_map.tsx | 174 - .../embeddables/low_poly_layer.json | 2898 ----------------- .../embeddables/map_config.test.ts | 47 - .../location_map/embeddables/map_config.ts | 175 - .../location_map/embeddables/map_tool_tip.tsx | 91 - .../location_map/embeddables/translations.ts | 15 - .../status_details/location_map/index.ts | 9 - .../location_map/location_map.test.tsx | 34 - .../location_map/location_map.tsx | 35 - .../location_map/location_missing.test.tsx | 22 - .../location_map/location_missing.tsx | 79 - 23 files changed, 50 insertions(+), 4425 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 24d5bd41b1ee4..14283aefd6149 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22898,7 +22898,6 @@ "xpack.uptime.certs.status.ok.label": " {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "スコア:{score}", "xpack.uptime.charts.mlAnnotation.severity": "深刻度:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "オブザーバー位置情報マップを監視", "xpack.uptime.controls.selectSeverity.criticalLabel": "致命的", "xpack.uptime.controls.selectSeverity.majorLabel": "メジャー", "xpack.uptime.controls.selectSeverity.minorLabel": "マイナー", @@ -22928,12 +22927,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "{title} を検索", "xpack.uptime.filterPopover.filterItem.label": "{title} {item}でフィルタリングします。", "xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "トグルを表示", - "xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。", - "xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。", - "xpack.uptime.locationMap.locations.missing.title": "地理情報の欠測", "xpack.uptime.locationName.helpLinkAnnotation": "場所を追加", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "キャンセル", @@ -23585,4 +23579,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 378b1bc1aa11a..50a3aeb5e0c44 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23257,7 +23257,6 @@ "xpack.uptime.certs.status.ok.label": " 对于 {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "分数:{score}", "xpack.uptime.charts.mlAnnotation.severity": "严重性:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "监测观察者位置地图", "xpack.uptime.controls.selectSeverity.criticalLabel": "紧急", "xpack.uptime.controls.selectSeverity.majorLabel": "重大", "xpack.uptime.controls.selectSeverity.minorLabel": "轻微", @@ -23287,12 +23286,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "搜索 {title}", "xpack.uptime.filterPopover.filterItem.label": "按 {title} {item} 筛选。", "xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "视图切换", - "xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。", - "xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。", - "xpack.uptime.locationMap.locations.missing.title": "地理信息缺失", "xpack.uptime.locationName.helpLinkAnnotation": "添加位置", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "取消", @@ -23954,4 +23948,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 426d3f1f10db8..4ba836c1e5d26 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,8 +1,16 @@ { - "configPath": ["xpack", "uptime"], + "configPath": [ + "xpack", + "uptime" + ], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["data", "home", "observability", "ml"], + "optionalPlugins": [ + "data", + "home", + "observability", + "ml" + ], "requiredPlugins": [ "alerting", "embeddable", @@ -14,5 +22,12 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "maps"] -} + "requiredBundles": [ + "observability", + "kibanaReact", + "kibanaUtils", + "home", + "data", + "ml" + ] +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts index 73ac77a61461f..2c95ac3347723 100644 --- a/x-pack/plugins/uptime/public/components/monitor/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/index.ts @@ -7,7 +7,6 @@ export * from './ml'; export * from './ping_list'; -export * from './status_details/location_map'; export * from './status_details'; export * from './ping_histogram'; export * from './monitor_charts'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap deleted file mode 100644 index 94cbeb49a32cf..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap +++ /dev/null @@ -1,234 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationAvailability component doesnt shows warning if geo is provided 1`] = ` - - - - - - - - - - - - - -`; - -exports[`LocationAvailability component renders correctly against snapshot 1`] = ` - - - - -

    - Monitoring from -

    - - - - - - - - - - - - -`; - -exports[`LocationAvailability component renders named locations that have missing geo data 1`] = ` - - - - - - - - - - - - - - - -`; - -exports[`LocationAvailability component shows warning if geo information is missing 1`] = ` - - - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx index 2edb2eec46580..855b8ef0c9767 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx @@ -6,28 +6,16 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../lib/helper/rtl_helpers'; import { LocationAvailability } from './location_availability'; import { MonitorLocations } from '../../../../../common/runtime_types'; -import { LocationMissingWarning } from '../location_map/location_missing'; // Note For shallow test, we need absolute time strings describe('LocationAvailability component', () => { let monitorLocations: MonitorLocations; - let localStorageMock: any; - - let selectedView = 'list'; beforeEach(() => { - localStorageMock = { - getItem: jest.fn().mockImplementation(() => selectedView), - setItem: jest.fn(), - }; - - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - }); - monitorLocations = { monitorId: 'wapo', up_history: 12, @@ -41,104 +29,34 @@ describe('LocationAvailability component', () => { down_history: 0, }, { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Unnamed-location' }, - timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, - }, - ], - }; - }); - - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('shows warning if geo information is missing', () => { - selectedView = 'map'; - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, + summary: { up: 2, down: 2 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, + up_history: 2, + down_history: 2, }, { - summary: { up: 4, down: 0 }, + summary: { up: 0, down: 4 }, geo: { name: 'Unnamed-location' }, timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, + up_history: 0, + down_history: 4, }, ], }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(1); }); - it('doesnt shows warning if geo is provided', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - ], - }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(0); - }); - - it('renders named locations that have missing geo data', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 4, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: undefined }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - ], - }; - - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); + it('renders correctly', () => { + render(); + expect(screen.getByRole('heading', { name: 'Monitoring from', level: 3 })); + expect(screen.getByText('New York')).toBeInTheDocument(); + expect(screen.getByText('Tokyo')).toBeInTheDocument(); + expect(screen.getByText('Unnamed-location')).toBeInTheDocument(); + expect(screen.getByText('100.00 %')).toBeInTheDocument(); + expect(screen.getByText('50.00 %')).toBeInTheDocument(); + expect(screen.getByText('0.00 %')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:06 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:04 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:02 PM')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx index 5f74098e12583..c851369d63e9e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiTitle } from '@elastic/eui'; import { LocationStatusTags } from '../availability_reporting'; -import { LocationPoint } from '../location_map/embeddables/embedded_map'; -import { MonitorLocations, MonitorLocation } from '../../../../../common/runtime_types'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; -import { LocationMissingWarning } from '../location_map/location_missing'; -import { useSelectedView } from './use_selected_view'; -import { LocationMap } from '../location_map'; +import { MonitorLocations } from '../../../../../common/runtime_types'; import { MonitoringFrom } from '../translations'; -import { ToggleViewBtn } from './toggle_view_btn'; const EuiFlexItemTags = styled(EuiFlexItem)` width: 350px; @@ -30,61 +24,20 @@ interface LocationMapProps { } export const LocationAvailability = ({ monitorLocations }: LocationMapProps) => { - const upPoints: LocationPoint[] = []; - const downPoints: LocationPoint[] = []; - - let isAnyGeoInfoMissing = false; - - if (monitorLocations?.locations) { - monitorLocations.locations.forEach(({ geo, summary }: MonitorLocation) => { - if (geo?.name === UNNAMED_LOCATION || !geo?.location) { - isAnyGeoInfoMissing = true; - } else if (!!geo.location.lat && !!geo.location.lon) { - if (summary?.down === 0) { - upPoints.push(geo as LocationPoint); - } else { - downPoints.push(geo as LocationPoint); - } - } - }); - } - const { selectedView: initialView } = useSelectedView(); - - const [selectedView, setSelectedView] = useState(initialView); - return ( - {selectedView === 'list' && ( - - -

    {MonitoringFrom}

    -
    -
    - )} - {selectedView === 'map' && ( - {isAnyGeoInfoMissing && } - )} - - { - setSelectedView(val); - }} - /> + + +

    {MonitoringFrom}

    +
    - {selectedView === 'list' && ( - - - - )} - {selectedView === 'map' && ( - - - - )} + + +
    ); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx deleted file mode 100644 index 45cb5c45bf021..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import styled from 'styled-components'; -import { EuiButtonGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSelectedView } from './use_selected_view'; -import { ChangeToListView, ChangeToMapView } from '../translations'; - -const ToggleViewButtons = styled.span` - margin-left: auto; -`; - -interface Props { - onChange: (val: string) => void; -} - -export const ToggleViewBtn = ({ onChange }: Props) => { - const toggleButtons = [ - { - id: `listBtn`, - label: ChangeToMapView, - name: 'listView', - iconType: 'list', - 'data-test-subj': 'uptimeMonitorToggleListBtn', - 'aria-label': ChangeToMapView, - }, - { - id: `mapBtn`, - label: ChangeToListView, - name: 'mapView', - iconType: 'mapMarker', - 'data-test-subj': 'uptimeMonitorToggleMapBtn', - 'aria-label': ChangeToListView, - }, - ]; - - const { selectedView, setSelectedView } = useSelectedView(); - - const onChangeView = (optionId: string) => { - const currView = optionId === 'listBtn' ? 'list' : 'map'; - setSelectedView(currView); - onChange(currView); - }; - - return ( - - onChangeView(id)} - type="multi" - isIconOnly - style={{ marginLeft: 'auto' }} - legend={i18n.translate('xpack.uptime.locationAvailabilityViewToggleLegend', { - defaultMessage: 'View toggle', - })} - /> - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts deleted file mode 100644 index fa77d0bf9057e..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; - -const localKey = 'xpack.uptime.detailPage.selectedView'; - -interface Props { - selectedView: string; - setSelectedView: (val: string) => void; -} - -export const useSelectedView = (): Props => { - const getSelectedView = localStorage.getItem(localKey) ?? 'list'; - - const [selectedView, setSelectedView] = useState(getSelectedView); - - useEffect(() => { - localStorage.setItem(localKey, selectedView); - }, [selectedView]); - - return { selectedView, setSelectedView }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap deleted file mode 100644 index 6b3d157c23fee..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMap component renders correctly against snapshot 1`] = ` - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap deleted file mode 100644 index 5e3e2e1a6db46..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMissingWarning component renders correctly against snapshot 1`] = ` -.c0 { - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -} - -
    -
    -
    -
    - -
    -
    -
    -
    -`; - -exports[`LocationMissingWarning component shallow render correctly against snapshot 1`] = ` - - - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - observer.geo.?? - , - } - } - /> - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts deleted file mode 100644 index b925697970a57..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from '../low_poly_layer.json'; - -export const mockDownPointsLayer = { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'Asia', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'APJ', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Canada', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#BC261E', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockUpPointsLayer = { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'US-EAST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'US-WEST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Europe', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockLayerList = [ - { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }, - mockDownPointsLayer, - mockUpPointsLayer, -]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx deleted file mode 100644 index 6706a435c7b6b..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState, useContext, useRef } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { - MapEmbeddable, - MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../maps/public/embeddable'; -import * as i18n from './translations'; -import { GeoPoint } from '../../../../../../common/runtime_types'; -import { getLayerList } from './map_config'; -import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../../contexts'; -import { - isErrorEmbeddable, - ViewMode, - ErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; -import { MapToolTipComponent } from './map_tool_tip'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; - -export interface EmbeddedMapProps { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export type LocationPoint = Required; - -const EmbeddedPanel = styled.div` - z-index: auto; - flex: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - .embPanel__content { - display: flex; - flex: 1 1 100%; - z-index: 1; - min-height: 0; // Absolute must for Firefox to scroll contents - } - &&& .mapboxgl-canvas { - animation: none !important; - } -`; - -export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { - const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState(); - const embeddableRoot: React.RefObject = useRef(null); - const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); - if (!embeddablePlugin) { - throw new Error('Embeddable start plugin not found'); - } - const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - - const portalNode = React.useMemo(() => createPortalNode(), []); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - attributes: { title: '' }, - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It wil also omit Greenland/Antarctica etc - mapCenter: { - lon: 11, - lat: 20, - zoom: 0, - }, - mapSettings: { - disableInteractive: true, - hideToolbarOverlay: true, - hideLayerControl: true, - hideViewControl: true, - }, - }; - - const renderTooltipContent = ({ - addFilters, - closeTooltip, - features, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }: RenderTooltipContentParams) => { - const props = { - addFilters, - closeTooltip, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }; - const relevantFeatures = features.filter( - (item: any) => item.layerId === 'up_points' || item.layerId === 'down_points' - ); - if (relevantFeatures.length > 0) { - return ; - } - closeTooltip(); - return null; - }; - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject: any = await factory.create({ - ...input, - title: i18n.MAP_TITLE, - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setRenderTooltipContent(renderTooltipContent); - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - - setEmbeddable(embeddableObject); - } - - setupEmbeddable(); - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update map layers based on points - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - }, [upPoints, downPoints, embeddable, colors]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot]); - - return ( - -
    - - - - - ); -}); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json deleted file mode 100644 index 7a309cd01ebc7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json +++ /dev/null @@ -1,2898 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "35.98361", - "34.52750" - ], - [ - "34.65943", - "36.80527" - ], - [ - "32.77166", - "36.02888" - ], - [ - "29.67722", - "36.11833" - ], - [ - "27.25500", - "36.96500" - ], - [ - "27.51166", - "40.30555" - ], - [ - "33.33860", - "42.01985" - ], - [ - "38.35582", - "40.91027" - ], - [ - "41.77609", - "41.84193" - ], - [ - "41.59748", - "43.22151" - ], - [ - "45.16512", - "42.70333" - ], - [ - "47.91547", - "41.22499" - ], - [ - "49.76062", - "42.71076" - ], - [ - "49.44831", - "45.53038" - ], - [ - "47.30249", - "50.03194" - ], - [ - "52.34180", - "51.78075" - ], - [ - "55.69249", - "50.53249" - ], - [ - "58.33777", - "51.15610" - ], - [ - "57.97027", - "54.38819" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.44912", - "58.48804" - ], - [ - "59.57756", - "63.93287" - ], - [ - "66.10887", - "67.48123" - ], - [ - "64.52222", - "68.90305" - ], - [ - "67.05498", - "68.85637" - ], - [ - "69.32735", - "72.94540" - ], - [ - "73.52553", - "71.81582" - ], - [ - "80.82610", - "72.08693" - ], - [ - "80.51860", - "73.57346" - ], - [ - "89.25278", - "75.50305" - ], - [ - "97.18359", - "75.92804" - ], - [ - "104.07138", - "77.73221" - ], - [ - "111.10387", - "76.75526" - ], - [ - "113.47054", - "73.50096" - ], - [ - "118.63443", - "73.57166" - ], - [ - "131.53580", - "70.87776" - ], - [ - "137.45190", - "71.34109" - ], - [ - "141.02414", - "72.58582" - ], - [ - "149.18524", - "72.22249" - ], - [ - "152.53830", - "70.83777" - ], - [ - "159.72968", - "69.83472" - ], - [ - "170.61194", - "68.75633" - ], - [ - "170.47189", - "70.13416" - ], - [ - "180.00000", - "68.98010" - ], - [ - "180.00000", - "65.06891" - ], - [ - "179.55373", - "62.61971" - ], - [ - "173.54178", - "61.74430" - ], - [ - "170.64194", - "60.41750" - ], - [ - "163.36023", - "59.82388" - ], - [ - "161.93858", - "58.06763" - ], - [ - "163.34996", - "56.19596" - ], - [ - "156.74524", - "51.07791" - ], - [ - "155.54413", - "55.30360" - ], - [ - "155.94206", - "56.65353" - ], - [ - "161.91248", - "60.41972" - ], - [ - "159.24747", - "61.92222" - ], - [ - "152.35718", - "59.02332" - ], - [ - "143.21109", - "59.37666" - ], - [ - "137.72580", - "56.17500" - ], - [ - "137.29327", - "54.07500" - ], - [ - "141.41483", - "53.29361" - ], - [ - "140.17609", - "48.45013" - ], - [ - "135.42233", - "43.75611" - ], - [ - "133.15485", - "42.68263" - ], - [ - "131.81052", - "43.32555" - ], - [ - "129.70204", - "40.83069" - ], - [ - "127.51763", - "39.73957" - ], - [ - "129.42944", - "37.05986" - ], - [ - "129.23749", - "35.18990" - ], - [ - "126.37556", - "34.79138" - ], - [ - "126.38860", - "37.88721" - ], - [ - "124.32395", - "39.91589" - ], - [ - "121.64804", - "38.99638" - ], - [ - "121.17747", - "40.92194" - ], - [ - "118.11053", - "38.14639" - ], - [ - "120.82054", - "36.64527" - ], - [ - "120.24873", - "34.31145" - ], - [ - "121.84693", - "30.85305" - ], - [ - "120.93526", - "27.98222" - ], - [ - "119.58074", - "25.67996" - ], - [ - "116.48172", - "22.93902" - ], - [ - "112.28194", - "21.70139" - ], - [ - "107.36693", - "21.26527" - ], - [ - "105.63857", - "18.89065" - ], - [ - "108.82916", - "15.42194" - ], - [ - "109.46186", - "12.86097" - ], - [ - "109.02168", - "11.36225" - ], - [ - "104.79893", - "8.79222" - ], - [ - "104.98177", - "10.10444" - ], - [ - "100.97635", - "13.46281" - ], - [ - "99.15082", - "10.36472" - ], - [ - "100.57809", - "7.22014" - ], - [ - "103.18192", - "5.28278" - ], - [ - "103.37455", - "1.53347" - ], - [ - "101.28574", - "2.84354" - ], - [ - "100.35553", - "5.96389" - ], - [ - "98.27415", - "8.27444" - ], - [ - "98.74720", - "11.67486" - ], - [ - "97.72457", - "15.84666" - ], - [ - "95.42859", - "15.72972" - ], - [ - "93.72436", - "19.93243" - ], - [ - "91.70444", - "22.48055" - ], - [ - "86.96332", - "21.38194" - ], - [ - "86.42123", - "19.98493" - ], - [ - "80.27943", - "15.69917" - ], - [ - "79.85811", - "10.28583" - ], - [ - "76.99860", - "8.36527" - ], - [ - "74.85526", - "12.75500" - ], - [ - "73.44748", - "16.05861" - ], - [ - "72.56485", - "21.37506" - ], - [ - "70.82513", - "20.69597" - ], - [ - "66.50005", - "25.40381" - ], - [ - "61.76083", - "25.03208" - ], - [ - "57.31909", - "25.77146" - ], - [ - "56.80888", - "27.12361" - ], - [ - "54.78846", - "26.49041" - ], - [ - "51.43027", - "27.93777" - ], - [ - "50.63916", - "29.47042" - ], - [ - "47.95943", - "30.03305" - ], - [ - "48.83887", - "27.61972" - ], - [ - "51.28236", - "24.30000" - ], - [ - "53.58777", - "24.04417" - ], - [ - "55.85944", - "25.72042" - ], - [ - "57.17131", - "23.93444" - ], - [ - "59.82861", - "22.29166" - ], - [ - "57.80569", - "18.97097" - ], - [ - "55.03194", - "17.01472" - ], - [ - "52.18916", - "15.60528" - ], - [ - "45.04232", - "12.75239" - ], - [ - "43.47888", - "12.67500" - ], - [ - "42.78933", - "16.46083" - ], - [ - "40.75694", - "19.76417" - ], - [ - "39.17486", - "21.10402" - ], - [ - "39.06277", - "22.58333" - ], - [ - "35.16055", - "28.05666" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "-169.69496", - "66.06806" - ], - [ - "-173.67308", - "64.34679" - ], - [ - "-179.32083", - "65.53012" - ], - [ - "-180.00000", - "65.06891" - ], - [ - "-180.00000", - "68.98010" - ], - [ - "-169.69496", - "66.06806" - ] - ] - ], - [ - [ - [ - "139.93851", - "40.42860" - ], - [ - "142.06970", - "39.54666" - ], - [ - "140.95358", - "38.14805" - ], - [ - "140.33218", - "35.12985" - ], - [ - "137.02879", - "34.56784" - ], - [ - "136.71246", - "36.75139" - ], - [ - "139.42622", - "38.15458" - ], - [ - "139.93851", - "40.42860" - ] - ] - ], - [ - [ - [ - "119.89259", - "15.80112" - ], - [ - "120.58527", - "18.51139" - ], - [ - "122.51833", - "17.04389" - ], - [ - "121.38026", - "15.30250" - ], - [ - "119.89259", - "15.80112" - ] - ] - ], - [ - [ - [ - "122.32916", - "7.30833" - ], - [ - "126.18610", - "9.24277" - ], - [ - "125.37762", - "6.72361" - ], - [ - "123.45888", - "7.81055" - ], - [ - "122.32916", - "7.30833" - ] - ] - ], - [ - [ - [ - "111.89638", - "-3.57389" - ], - [ - "110.23193", - "-2.97111" - ], - [ - "108.84549", - "0.81056" - ], - [ - "109.64857", - "2.07341" - ], - [ - "113.01054", - "3.16055" - ], - [ - "115.37886", - "4.91167" - ], - [ - "116.75417", - "7.01805" - ], - [ - "119.27582", - "5.34500" - ], - [ - "117.27540", - "3.22000" - ], - [ - "117.87192", - "1.87667" - ], - [ - "117.44479", - "-0.52397" - ], - [ - "115.96624", - "-3.60875" - ], - [ - "113.03471", - "-2.98972" - ], - [ - "111.89638", - "-3.57389" - ] - ] - ], - [ - [ - [ - "102.97601", - "0.64348" - ], - [ - "103.36081", - "-0.70222" - ], - [ - "106.05525", - "-3.03139" - ], - [ - "105.72887", - "-5.89826" - ], - [ - "102.32610", - "-4.00611" - ], - [ - "100.90555", - "-2.31944" - ], - [ - "98.70383", - "1.55979" - ], - [ - "95.53108", - "4.68278" - ], - [ - "97.51483", - "5.24944" - ], - [ - "100.41219", - "2.29306" - ], - [ - "102.97601", - "0.64348" - ] - ] - ], - [ - [ - [ - "120.82723", - "1.23406" - ], - [ - "120.01999", - "-0.07528" - ], - [ - "122.47623", - "-3.16090" - ], - [ - "120.32888", - "-5.51208" - ], - [ - "119.35491", - "-5.40007" - ], - [ - "118.88860", - "-2.89319" - ], - [ - "119.77805", - "0.22972" - ], - [ - "120.82723", - "1.23406" - ] - ] - ], - [ - [ - [ - "136.04913", - "-2.69806" - ], - [ - "137.87579", - "-1.47306" - ], - [ - "144.51373", - "-3.82222" - ], - [ - "145.76639", - "-5.48528" - ], - [ - "147.46661", - "-5.97086" - ], - [ - "146.08969", - "-8.09111" - ], - [ - "144.21738", - "-7.79465" - ], - [ - "143.36510", - "-9.01222" - ], - [ - "141.11996", - "-9.23097" - ], - [ - "139.09454", - "-7.56181" - ], - [ - "138.06525", - "-5.40896" - ], - [ - "135.20468", - "-4.45972" - ], - [ - "132.72275", - "-2.81722" - ], - [ - "131.25555", - "-0.82278" - ], - [ - "134.02950", - "-0.96694" - ], - [ - "134.99495", - "-3.33653" - ], - [ - "136.04913", - "-2.69806" - ] - ] - ], - [ - [ - [ - "110.05640", - "-7.89751" - ], - [ - "106.56721", - "-7.41694" - ], - [ - "106.07582", - "-5.88194" - ], - [ - "110.39360", - "-6.97903" - ], - [ - "110.05640", - "-7.89751" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Asia" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "-25.28167", - "71.39166" - ], - [ - "-23.56056", - "70.10609" - ], - [ - "-26.36333", - "68.66748" - ], - [ - "-31.99916", - "68.09526" - ], - [ - "-34.71999", - "66.33832" - ], - [ - "-41.15541", - "64.96235" - ], - [ - "-43.08722", - "60.10027" - ], - [ - "-47.68986", - "61.00680" - ], - [ - "-50.31562", - "62.49430" - ], - [ - "-53.23333", - "65.68283" - ], - [ - "-53.62778", - "67.81470" - ], - [ - "-50.58930", - "69.92373" - ], - [ - "-54.68694", - "72.36721" - ], - [ - "-58.15958", - "75.50860" - ], - [ - "-68.50056", - "76.08693" - ], - [ - "-72.55222", - "78.52110" - ], - [ - "-60.80666", - "81.87997" - ], - [ - "-30.38833", - "83.60220" - ], - [ - "-16.00500", - "80.72859" - ], - [ - "-22.03695", - "77.68568" - ], - [ - "-19.33681", - "75.40207" - ], - [ - "-24.46305", - "73.53581" - ], - [ - "-25.28167", - "71.39166" - ] - ] - ], - [ - [ - [ - "-87.64890", - "76.33804" - ], - [ - "-86.47916", - "79.76167" - ], - [ - "-90.43666", - "81.88750" - ], - [ - "-70.26001", - "83.11388" - ], - [ - "-61.07639", - "82.32083" - ], - [ - "-78.78194", - "76.57221" - ], - [ - "-87.64890", - "76.33804" - ] - ] - ], - [ - [ - [ - "-123.83389", - "73.70027" - ], - [ - "-115.31903", - "73.47707" - ], - [ - "-123.29306", - "71.14610" - ], - [ - "-123.83389", - "73.70027" - ] - ] - ], - [ - [ - [ - "-65.32806", - "62.66610" - ], - [ - "-68.61583", - "62.26389" - ], - [ - "-77.33667", - "65.17609" - ], - [ - "-72.25835", - "67.24803" - ], - [ - "-77.30506", - "69.83395" - ], - [ - "-85.87465", - "70.07943" - ], - [ - "-89.90348", - "71.35304" - ], - [ - "-89.03958", - "73.25499" - ], - [ - "-81.57251", - "73.71971" - ], - [ - "-67.21986", - "69.94081" - ], - [ - "-67.23819", - "68.35790" - ], - [ - "-61.26458", - "66.62609" - ], - [ - "-65.56204", - "64.73154" - ], - [ - "-65.32806", - "62.66610" - ] - ] - ], - [ - [ - [ - "-105.02444", - "72.21999" - ], - [ - "-100.99973", - "70.17276" - ], - [ - "-101.85139", - "68.98442" - ], - [ - "-113.04173", - "68.49374" - ], - [ - "-116.53221", - "69.40887" - ], - [ - "-119.13445", - "71.77457" - ], - [ - "-114.66666", - "73.37247" - ], - [ - "-105.02444", - "72.21999" - ] - ] - ], - [ - [ - [ - "-77.36667", - "8.67500" - ], - [ - "-77.88972", - "7.22889" - ], - [ - "-79.69778", - "8.86666" - ], - [ - "-81.73862", - "8.16250" - ], - [ - "-85.65668", - "9.90500" - ], - [ - "-85.66959", - "11.05500" - ], - [ - "-87.93779", - "13.15639" - ], - [ - "-91.38474", - "13.97889" - ], - [ - "-93.93861", - "16.09389" - ], - [ - "-96.47612", - "15.64361" - ], - [ - "-103.45001", - "18.31361" - ], - [ - "-105.67834", - "20.38305" - ], - [ - "-105.18945", - "21.43750" - ], - [ - "-106.91570", - "23.86514" - ], - [ - "-109.43750", - "25.82027" - ], - [ - "-109.44431", - "26.71555" - ], - [ - "-112.16195", - "28.97139" - ], - [ - "-113.09167", - "31.22972" - ], - [ - "-115.69667", - "29.77423" - ], - [ - "-117.40944", - "33.24416" - ], - [ - "-120.60583", - "34.55860" - ], - [ - "-124.33118", - "40.27246" - ], - [ - "-124.52444", - "42.86610" - ], - [ - "-123.87161", - "45.52898" - ], - [ - "-124.71431", - "48.39708" - ], - [ - "-124.03510", - "49.91801" - ], - [ - "-127.17315", - "50.92221" - ], - [ - "-130.88640", - "55.70791" - ], - [ - "-133.81302", - "57.97293" - ], - [ - "-136.65891", - "58.21652" - ], - [ - "-140.40335", - "59.69804" - ], - [ - "-146.75543", - "60.95249" - ], - [ - "-154.23567", - "58.13069" - ], - [ - "-157.55139", - "58.38777" - ], - [ - "-165.42244", - "60.55215" - ], - [ - "-164.40112", - "63.21499" - ], - [ - "-168.13196", - "65.66296" - ], - [ - "-161.66779", - "67.02054" - ], - [ - "-166.82362", - "68.34873" - ], - [ - "-156.59673", - "71.35144" - ], - [ - "-151.22986", - "70.37296" - ], - [ - "-143.21555", - "70.11026" - ], - [ - "-137.25500", - "68.94832" - ], - [ - "-127.18096", - "70.27638" - ], - [ - "-114.06652", - "68.46970" - ], - [ - "-112.39584", - "67.67915" - ], - [ - "-98.11124", - "67.83887" - ], - [ - "-90.43639", - "68.87442" - ], - [ - "-85.55499", - "69.85970" - ], - [ - "-81.33570", - "69.18498" - ], - [ - "-81.50222", - "67.00096" - ], - [ - "-85.89726", - "66.16802" - ], - [ - "-87.98736", - "64.18845" - ], - [ - "-92.71001", - "62.46583" - ], - [ - "-94.78972", - "59.09222" - ], - [ - "-92.41875", - "57.33270" - ], - [ - "-88.81500", - "56.82444" - ], - [ - "-85.00195", - "55.29666" - ], - [ - "-82.30777", - "55.14888" - ], - [ - "-82.27390", - "52.95638" - ], - [ - "-78.57945", - "52.11138" - ], - [ - "-79.76181", - "54.65166" - ], - [ - "-76.67979", - "56.03645" - ], - [ - "-78.57299", - "58.62888" - ], - [ - "-77.50835", - "62.56166" - ], - [ - "-73.68346", - "62.47999" - ], - [ - "-70.14848", - "61.08458" - ], - [ - "-67.56610", - "58.22360" - ], - [ - "-64.74538", - "60.23075" - ], - [ - "-61.09055", - "55.84415" - ], - [ - "-57.34969", - "54.57496" - ], - [ - "-56.95160", - "51.42458" - ], - [ - "-60.00500", - "50.24888" - ], - [ - "-66.44903", - "50.26777" - ], - [ - "-64.21167", - "48.88499" - ], - [ - "-64.90430", - "46.84597" - ], - [ - "-63.66708", - "45.81666" - ], - [ - "-70.19187", - "43.57555" - ], - [ - "-70.72610", - "41.72777" - ], - [ - "-74.13390", - "40.70082" - ], - [ - "-75.96083", - "37.15221" - ], - [ - "-76.34326", - "34.88194" - ], - [ - "-78.82750", - "33.73027" - ], - [ - "-81.48843", - "31.11347" - ], - [ - "-80.03534", - "26.79569" - ], - [ - "-81.73659", - "25.95944" - ], - [ - "-84.01098", - "30.09764" - ], - [ - "-88.98083", - "30.41833" - ], - [ - "-94.75417", - "29.36791" - ], - [ - "-97.56041", - "26.84208" - ], - [ - "-97.74223", - "22.01250" - ], - [ - "-95.80112", - "18.74500" - ], - [ - "-94.46918", - "18.14625" - ], - [ - "-90.73167", - "19.36153" - ], - [ - "-90.27972", - "21.06305" - ], - [ - "-86.82973", - "21.42923" - ], - [ - "-88.28250", - "17.62389" - ], - [ - "-88.13696", - "15.68285" - ], - [ - "-84.26015", - "15.82597" - ], - [ - "-83.18695", - "14.32389" - ], - [ - "-83.84751", - "11.17458" - ], - [ - "-82.24278", - "9.00236" - ], - [ - "-79.53445", - "9.62014" - ], - [ - "-77.36667", - "8.67500" - ] - ] - ], - [ - [ - [ - "-55.19333", - "46.98499" - ], - [ - "-59.40361", - "47.89423" - ], - [ - "-56.68250", - "51.33943" - ], - [ - "-55.56114", - "49.36818" - ], - [ - "-52.83465", - "48.09965" - ], - [ - "-55.19333", - "46.98499" - ] - ] - ], - [ - [ - [ - "-73.03644", - "18.45622" - ], - [ - "-72.79834", - "19.94278" - ], - [ - "-69.94932", - "19.67680" - ], - [ - "-68.89528", - "18.39639" - ], - [ - "-73.03644", - "18.45622" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "North America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "64.52222", - "68.90305" - ], - [ - "66.10887", - "67.48123" - ], - [ - "59.57756", - "63.93287" - ], - [ - "59.44912", - "58.48804" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.97027", - "54.38819" - ], - [ - "58.33777", - "51.15610" - ], - [ - "55.69249", - "50.53249" - ], - [ - "52.34180", - "51.78075" - ], - [ - "47.30249", - "50.03194" - ], - [ - "49.44831", - "45.53038" - ], - [ - "49.76062", - "42.71076" - ], - [ - "47.91547", - "41.22499" - ], - [ - "45.16512", - "42.70333" - ], - [ - "41.59748", - "43.22151" - ], - [ - "39.94553", - "43.39693" - ], - [ - "34.70249", - "46.17582" - ], - [ - "30.83277", - "46.54832" - ], - [ - "28.78083", - "44.66096" - ], - [ - "28.01305", - "41.98222" - ], - [ - "26.36041", - "40.95388" - ], - [ - "22.59500", - "40.01221" - ], - [ - "23.96055", - "38.28166" - ], - [ - "22.15246", - "37.01854" - ], - [ - "19.30721", - "40.64531" - ], - [ - "19.59771", - "41.80611" - ], - [ - "15.15167", - "44.19639" - ], - [ - "13.02958", - "41.26014" - ], - [ - "8.74722", - "44.42805" - ], - [ - "6.16528", - "43.05055" - ], - [ - "4.05625", - "43.56277" - ], - [ - "3.20167", - "41.89278" - ], - [ - "0.99306", - "41.04805" - ], - [ - "0.20722", - "38.73221" - ], - [ - "-2.12292", - "36.73347" - ], - [ - "-5.61361", - "36.00610" - ], - [ - "-6.95992", - "37.22184" - ], - [ - "-8.98924", - "37.02631" - ], - [ - "-9.49083", - "38.79388" - ], - [ - "-8.66014", - "40.69111" - ], - [ - "-9.16972", - "43.18583" - ], - [ - "-1.44389", - "43.64055" - ], - [ - "-1.11463", - "46.31658" - ], - [ - "-2.68528", - "48.50166" - ], - [ - "1.43875", - "50.10083" - ], - [ - "5.59917", - "53.30028" - ], - [ - "13.80854", - "53.85479" - ], - [ - "21.24506", - "54.95506" - ], - [ - "21.05223", - "56.81749" - ], - [ - "23.43159", - "59.95382" - ], - [ - "21.42416", - "60.57930" - ], - [ - "21.58500", - "64.43971" - ], - [ - "17.09861", - "61.60278" - ], - [ - "19.07264", - "59.73819" - ], - [ - "16.37982", - "56.66333" - ], - [ - "12.46007", - "56.29666" - ], - [ - "10.51569", - "59.30624" - ], - [ - "8.12750", - "58.09888" - ], - [ - "5.50847", - "58.66764" - ], - [ - "4.94944", - "61.41041" - ], - [ - "9.54528", - "63.76611" - ], - [ - "15.28833", - "68.03055" - ], - [ - "21.30000", - "70.24693" - ], - [ - "28.20778", - "71.07999" - ], - [ - "32.80605", - "69.30277" - ], - [ - "43.75180", - "67.31152" - ], - [ - "53.60437", - "68.90818" - ], - [ - "64.52222", - "68.90305" - ] - ] - ], - [ - [ - [ - "-13.49944", - "65.06915" - ], - [ - "-18.77500", - "63.39139" - ], - [ - "-22.04556", - "64.04666" - ], - [ - "-22.42167", - "66.43332" - ], - [ - "-16.41736", - "66.27603" - ], - [ - "-13.49944", - "65.06915" - ] - ] - ], - [ - [ - [ - "-4.19667", - "57.48583" - ], - [ - "-0.07931", - "54.11340" - ], - [ - "0.25389", - "50.73861" - ], - [ - "-3.43722", - "50.60500" - ], - [ - "-4.19639", - "53.20611" - ], - [ - "-2.89979", - "53.72499" - ], - [ - "-6.22778", - "56.69722" - ], - [ - "-4.19667", - "57.48583" - ] - ] - ], - [ - [ - [ - "12.44167", - "37.80611" - ], - [ - "15.64794", - "38.26458" - ], - [ - "15.08139", - "36.64916" - ], - [ - "12.44167", - "37.80611" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Europe" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "34.90380", - "29.48671" - ], - [ - "33.93833", - "26.65528" - ], - [ - "36.88625", - "22.05319" - ], - [ - "37.43569", - "18.85389" - ], - [ - "38.58902", - "18.06680" - ], - [ - "39.71805", - "15.08805" - ], - [ - "41.17222", - "14.63069" - ], - [ - "43.32750", - "12.47673" - ], - [ - "44.27833", - "10.44778" - ], - [ - "50.09319", - "11.51458" - ], - [ - "51.14555", - "10.63361" - ], - [ - "48.00055", - "4.52306" - ], - [ - "46.02555", - "2.43722" - ], - [ - "43.48861", - "0.65000" - ], - [ - "40.12548", - "-3.26569" - ], - [ - "38.77611", - "-6.03972" - ], - [ - "40.38777", - "-11.31778" - ], - [ - "40.57833", - "-15.49889" - ], - [ - "34.89069", - "-19.86042" - ], - [ - "35.45611", - "-24.16945" - ], - [ - "32.81111", - "-25.61209" - ], - [ - "32.39444", - "-28.53139" - ], - [ - "27.90000", - "-33.04056" - ], - [ - "24.82472", - "-34.20167" - ], - [ - "22.53916", - "-34.01118" - ], - [ - "20.00000", - "-34.82200" - ], - [ - "17.84750", - "-32.83083" - ], - [ - "18.21791", - "-31.73458" - ], - [ - "15.09500", - "-26.73528" - ], - [ - "14.51139", - "-22.55278" - ], - [ - "11.76764", - "-17.98820" - ], - [ - "11.73125", - "-15.85070" - ], - [ - "13.84944", - "-10.95611" - ], - [ - "13.39180", - "-8.39375" - ], - [ - "11.77417", - "-4.54264" - ], - [ - "9.70250", - "-2.44792" - ], - [ - "9.29833", - "-0.37167" - ], - [ - "9.96514", - "3.08521" - ], - [ - "8.89861", - "4.58833" - ], - [ - "5.93583", - "4.33833" - ], - [ - "4.41021", - "6.35993" - ], - [ - "1.46889", - "6.18639" - ], - [ - "-2.05889", - "4.73083" - ], - [ - "-4.46806", - "5.29556" - ], - [ - "-7.43639", - "4.34917" - ], - [ - "-9.23889", - "5.12278" - ], - [ - "-12.50417", - "7.38861" - ], - [ - "-13.49313", - "9.56008" - ], - [ - "-15.00542", - "10.77194" - ], - [ - "-17.17556", - "14.65444" - ], - [ - "-16.03945", - "17.73458" - ], - [ - "-16.91625", - "21.94542" - ], - [ - "-12.96271", - "27.92048" - ], - [ - "-11.51195", - "28.30375" - ], - [ - "-9.64097", - "30.16500" - ], - [ - "-8.53833", - "33.25055" - ], - [ - "-6.84306", - "34.01861" - ], - [ - "-5.91874", - "35.79065" - ], - [ - "-1.97972", - "35.07333" - ], - [ - "1.18250", - "36.51221" - ], - [ - "9.85868", - "37.32833" - ], - [ - "11.12667", - "35.24194" - ], - [ - "11.17430", - "33.21006" - ], - [ - "15.16583", - "32.39861" - ], - [ - "15.75430", - "31.38972" - ], - [ - "18.95750", - "30.27639" - ], - [ - "20.56763", - "32.56091" - ], - [ - "29.03500", - "30.82417" - ], - [ - "30.35545", - "31.50284" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "48.03140", - "-14.06341" - ], - [ - "49.94333", - "-13.03945" - ], - [ - "50.48277", - "-15.40583" - ], - [ - "49.36833", - "-18.35139" - ], - [ - "47.13305", - "-24.92806" - ], - [ - "44.01708", - "-24.98083" - ], - [ - "43.23888", - "-22.28250" - ], - [ - "44.48277", - "-19.96584" - ], - [ - "43.93139", - "-17.50056" - ], - [ - "44.87360", - "-16.21028" - ], - [ - "48.03140", - "-14.06341" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Africa" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "-77.88972", - "7.22889" - ], - [ - "-77.36667", - "8.67500" - ], - [ - "-75.63432", - "9.44819" - ], - [ - "-74.86081", - "11.12549" - ], - [ - "-68.84368", - "11.44708" - ], - [ - "-68.11424", - "10.48493" - ], - [ - "-61.87959", - "10.72833" - ], - [ - "-61.61987", - "9.90528" - ], - [ - "-57.51919", - "6.27077" - ], - [ - "-52.97320", - "5.47305" - ], - [ - "-51.25931", - "4.15250" - ], - [ - "-49.90320", - "1.17444" - ], - [ - "-51.92751", - "-1.33486" - ], - [ - "-48.42722", - "-1.66028" - ], - [ - "-47.28556", - "-0.59917" - ], - [ - "-42.23584", - "-2.83778" - ], - [ - "-39.99875", - "-2.84653" - ], - [ - "-37.17445", - "-4.91861" - ], - [ - "-35.47973", - "-5.16611" - ], - [ - "-34.83129", - "-6.98180" - ], - [ - "-35.32751", - "-9.22889" - ], - [ - "-39.05709", - "-13.38028" - ], - [ - "-38.87195", - "-15.87417" - ], - [ - "-39.70403", - "-19.42361" - ], - [ - "-42.03445", - "-22.91917" - ], - [ - "-44.67521", - "-23.05570" - ], - [ - "-48.02612", - "-25.01500" - ], - [ - "-48.84251", - "-28.61778" - ], - [ - "-52.21764", - "-31.74500" - ], - [ - "-54.14077", - "-34.66466" - ], - [ - "-56.15834", - "-34.92722" - ], - [ - "-56.67834", - "-36.92361" - ], - [ - "-58.30112", - "-38.48500" - ], - [ - "-62.06875", - "-39.50848" - ], - [ - "-62.39001", - "-40.90195" - ], - [ - "-65.13014", - "-40.84417" - ], - [ - "-65.24945", - "-44.31306" - ], - [ - "-67.58435", - "-46.00030" - ], - [ - "-65.78979", - "-47.96584" - ], - [ - "-68.94112", - "-50.38806" - ], - [ - "-68.99014", - "-51.62445" - ], - [ - "-72.11501", - "-53.68764" - ], - [ - "-74.28924", - "-50.48049" - ], - [ - "-74.74139", - "-47.71146" - ], - [ - "-72.61389", - "-44.47278" - ], - [ - "-73.99432", - "-40.96695" - ], - [ - "-73.22404", - "-39.41688" - ], - [ - "-73.67709", - "-37.34729" - ], - [ - "-71.44667", - "-32.66500" - ], - [ - "-71.69585", - "-30.50667" - ], - [ - "-70.91389", - "-27.62445" - ], - [ - "-70.05334", - "-21.42565" - ], - [ - "-70.31202", - "-18.43750" - ], - [ - "-71.49424", - "-17.30223" - ], - [ - "-75.05139", - "-15.46597" - ], - [ - "-76.39480", - "-13.88417" - ], - [ - "-78.99459", - "-8.21965" - ], - [ - "-81.17473", - "-6.08667" - ], - [ - "-81.27640", - "-4.28083" - ], - [ - "-79.95632", - "-3.20778" - ], - [ - "-80.91279", - "-1.03653" - ], - [ - "-80.10084", - "0.77028" - ], - [ - "-78.88929", - "1.23837" - ], - [ - "-77.43445", - "4.03139" - ], - [ - "-77.88972", - "7.22889" - ] - ] - ] - }, - "properties": { - "CONTINENT": "South America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "177.91779", - "-38.94280" - ], - [ - "175.95523", - "-41.25528" - ], - [ - "173.75165", - "-39.27000" - ], - [ - "174.94025", - "-38.10111" - ], - [ - "177.91779", - "-38.94280" - ] - ] - ], - [ - [ - [ - "171.18524", - "-44.93833" - ], - [ - "169.45801", - "-46.62333" - ], - [ - "166.47690", - "-45.80972" - ], - [ - "168.37233", - "-44.04056" - ], - [ - "171.15166", - "-42.56042" - ], - [ - "172.63025", - "-40.51056" - ], - [ - "174.23636", - "-41.83722" - ], - [ - "171.18524", - "-44.93833" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Oceania" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "151.54025", - "-24.04583" - ], - [ - "153.18192", - "-25.94944" - ], - [ - "153.62419", - "-28.66104" - ], - [ - "152.52969", - "-32.40361" - ], - [ - "151.45456", - "-33.31681" - ], - [ - "149.97163", - "-37.52222" - ], - [ - "146.87357", - "-38.65166" - ], - [ - "143.54295", - "-38.85923" - ], - [ - "140.52997", - "-38.00028" - ], - [ - "138.09225", - "-34.13493" - ], - [ - "135.49586", - "-34.61708" - ], - [ - "134.18414", - "-32.48666" - ], - [ - "131.14859", - "-31.47403" - ], - [ - "125.97227", - "-32.26674" - ], - [ - "123.73499", - "-33.77972" - ], - [ - "120.00499", - "-33.92889" - ], - [ - "117.93414", - "-35.12534" - ], - [ - "115.00895", - "-34.26243" - ], - [ - "115.73998", - "-31.86806" - ], - [ - "113.64346", - "-26.65431" - ], - [ - "113.38971", - "-24.42944" - ], - [ - "114.03027", - "-21.84167" - ], - [ - "116.70749", - "-20.64917" - ], - [ - "121.02748", - "-19.59222" - ], - [ - "122.95623", - "-16.58681" - ], - [ - "126.85790", - "-13.75097" - ], - [ - "129.08942", - "-14.89944" - ], - [ - "130.57927", - "-12.40465" - ], - [ - "132.67198", - "-11.50813" - ], - [ - "135.23135", - "-12.29445" - ], - [ - "135.45135", - "-14.93278" - ], - [ - "136.76581", - "-15.90445" - ], - [ - "140.83330", - "-17.45194" - ], - [ - "141.66553", - "-15.02653" - ], - [ - "141.59412", - "-12.53167" - ], - [ - "142.78830", - "-11.08056" - ], - [ - "143.78220", - "-14.41333" - ], - [ - "145.31580", - "-14.94555" - ], - [ - "146.27762", - "-18.88701" - ], - [ - "147.43192", - "-19.41236" - ], - [ - "150.81912", - "-22.73194" - ], - [ - "151.54025", - "-24.04583" - ] - ] - ] - }, - "properties": { - "CONTINENT": "Australia" - } - } - ] -} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts deleted file mode 100644 index 5ad92d4e6d1d7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getLayerList } from './map_config'; -import { mockLayerList } from './__mocks__/poly_layer_mock'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -jest.mock('uuid', () => { - return { - v4: jest.fn(() => 'uuid.v4()'), - }; -}); - -describe('map_config', () => { - let upPoints: LocationPoint[]; - let downPoints: LocationPoint[]; - let colors: Pick; - - beforeEach(() => { - upPoints = [ - { name: 'US-EAST', location: { lat: '52.487239', lon: '13.399262' } }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'US-WEST' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Europe' }, - ]; - downPoints = [ - { location: { lat: '52.487239', lon: '13.399262' }, name: 'Asia' }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'APJ' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Canada' }, - ]; - colors = { - danger: '#BC261E', - gray: '#000', - }; - }); - - describe('#getLayerList', () => { - test('it returns the low poly layer', () => { - const layerList = getLayerList(upPoints, downPoints, colors); - expect(layerList).toStrictEqual(mockLayerList); - }); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts deleted file mode 100644 index 723eee6f14b80..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -/** - * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, - * destination, and line layer for each of the provided indexPatterns - * - */ -export const getLayerList = ( - upPoints: LocationPoint[], - downPoints: LocationPoint[], - { danger }: Pick -) => { - return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; -}; - -export const getLowPolyLayer = () => { - return { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => { - const features = downPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: dangerColor, - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getUpPointsLayer = (upPoints: LocationPoint[]) => { - const features = upPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx deleted file mode 100644 index c03ed94f8c544..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; -import { useSelector } from 'react-redux'; -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiOutsideClickDetector, - EuiPopoverTitle, -} from '@elastic/eui'; -import { TagLabel } from '../../availability_reporting'; -import { UptimeThemeContext } from '../../../../../contexts'; -import { AppState } from '../../../../../state'; -import { monitorLocationsSelector } from '../../../../../state/selectors'; -import { useMonitorId } from '../../../../../hooks'; -import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -import type { RenderTooltipContentParams } from '../../../../../../../maps/public'; -import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; -import { LastCheckLabel } from '../../translations'; - -type MapToolTipProps = Partial; - -export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipProps) => { - const { id: featureId, layerId } = features[0] ?? {}; - const locationName = featureId?.toString(); - const { - colors: { gray, danger }, - } = useContext(UptimeThemeContext); - - const monitorId = useMonitorId(); - - const monitorLocations = useSelector((state: AppState) => - monitorLocationsSelector(state, monitorId) - ); - if (!locationName || !monitorLocations?.locations) { - return null; - } - const { - timestamp, - up_history: ups, - down_history: downs, - }: MonitorLocation = monitorLocations.locations!.find( - ({ geo }: MonitorLocation) => geo.name === locationName - )!; - - const availability = (ups / (ups + downs)) * 100; - - return ( - { - if (closeTooltip != null) { - closeTooltip(); - } - }} - > - <> - - {layerId === 'up_points' ? ( - - ) : ( - - )} - - - Availability - - {i18n.translate('xpack.uptime.mapToolTip.AvailabilityStat.title', { - defaultMessage: '{value} %', - values: { value: formatAvailabilityValue(availability) }, - description: 'A percentage value like 23.5%', - })} - - {LastCheckLabel} - - {moment(timestamp).fromNow()} - - - - - ); -}; - -export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts deleted file mode 100644 index edbf2b5b5e864..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const MAP_TITLE = i18n.translate( - 'xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle', - { - defaultMessage: 'Monitor Observer Location Map', - } -); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts deleted file mode 100644 index 650a0a9b82391..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './location_map'; -export * from '../availability_reporting/location_status_tags'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx deleted file mode 100644 index 9818fc164193c..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { LocationMap } from './location_map'; -import { LocationPoint } from './embeddables/embedded_map'; - -// Note For shallow test, we need absolute time strings -describe('LocationMap component', () => { - let upPoints: LocationPoint[]; - - beforeEach(() => { - upPoints = [ - { - name: 'New York', - location: { lat: '40.730610', lon: ' -73.935242' }, - }, - { - name: 'Tokyo', - location: { lat: '52.487448', lon: ' 13.394798' }, - }, - ]; - }); - - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx deleted file mode 100644 index 5a912a44b7c9a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; - -// These height/width values are used to make sure map is in center of panel -// And to make sure, it doesn't take too much space -const MapPanel = styled.div` - height: 240px; - width: 520px; - margin-right: 65px; - @media (max-width: 574px) { - height: 250px; - width: 100%; - } -`; - -interface Props { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export const LocationMap = ({ upPoints, downPoints }: Props) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx deleted file mode 100644 index dad7a61e74999..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { LocationMissingWarning } from './location_missing'; - -describe('LocationMissingWarning component', () => { - it('shallow render correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('renders correctly against snapshot', () => { - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx deleted file mode 100644 index 7b03f516decad..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiSpacer, - EuiText, - EuiCode, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { LocationLink } from '../../../common/location_link'; - -const EuiPopoverRight = styled(EuiFlexItem)` - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -`; - -export const LocationMissingWarning = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const togglePopover = () => { - setIsPopoverOpen(!isPopoverOpen); - }; - - const button = ( - - - - ); - - return ( - - - - - observer.geo.?? }} - /> - - - - - - - - - - - ); -}; From 869fd9355a281451174453b56fb690e5a9e298b0 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 8 Apr 2021 13:36:45 -0500 Subject: [PATCH 129/131] skip a11y spaces tests. #77933, #96625 --- x-pack/test/accessibility/apps/spaces.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index dc0dfad2debf6..a2f0e835c0b3e 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -18,13 +18,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - describe('Kibana spaces page meets a11y validations', () => { + // flaky + // https://github.com/elastic/kibana/issues/77933 + // https://github.com/elastic/kibana/issues/96625 + describe.skip('Kibana spaces page meets a11y validations', () => { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.common.navigateToApp('home'); }); - // flaky https://github.com/elastic/kibana/issues/77933 it.skip('a11y test for manage spaces menu from top nav on Kibana home', async () => { await PageObjects.spaceSelector.openSpacesNav(); await retry.waitFor( @@ -34,7 +36,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // flaky it.skip('a11y test for manage spaces page', async () => { await PageObjects.spaceSelector.clickManageSpaces(); await PageObjects.header.waitUntilLoadingHasFinished(); From 4af344a9b0b3301d97d91e1ab1dabf0adb8f6e68 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Thu, 8 Apr 2021 19:44:57 +0100 Subject: [PATCH 130/131] Improved role management error handling for partially authorized users (#96468) * Role management: Gracefully handle underprivileged users * Removed redundant condition --- .../roles/edit_role/edit_role_page.test.tsx | 28 ++++++++++--------- .../roles/edit_role/edit_role_page.tsx | 8 ++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 3002db642bc1e..5df73f7f8ec4e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -480,23 +480,25 @@ describe('', () => { }); }); - it('can render if features are not available', async () => { - const { http } = coreMock.createStart(); - http.get.mockImplementation(async (path: any) => { - if (path === '/api/features') { - const error = { response: { status: 404 } }; - throw error; - } + it('registers fatal error if features endpoint fails unexpectedly', async () => { + const error = { response: { status: 500 } }; + const getFeatures = jest.fn().mockRejectedValue(error); + const props = getProps({ action: 'edit' }); + const wrapper = mountWithIntl(); - if (path === '/api/spaces/space') { - return buildSpaces(); - } - }); + await waitForRender(wrapper); + expect(props.fatalErrors.add).toHaveBeenLastCalledWith(error); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(0); + }); - const wrapper = mountWithIntl(); + it('can render if features call is not allowed', async () => { + const error = { response: { status: 403 } }; + const getFeatures = jest.fn().mockRejectedValue(error); + const props = getProps({ action: 'edit' }); + const wrapper = mountWithIntl(); await waitForRender(wrapper); - + expect(props.fatalErrors.add).not.toHaveBeenCalled(); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expectSaveFormButtons(wrapper); 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 5d6b4a1b4fdaf..f810cd2079d16 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 @@ -256,13 +256,12 @@ function useFeatures( // possible that a user with `manage_security` will attempt to visit the role management page without the // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, - // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the - // 404 here, and respond in a way that still allows the UI to render itself. - const unauthorizedForFeatures = err.response?.status === 404; + // this API endpoint will throw a 403, which causes view to fail completely. So we instead attempt to detect the + // 403 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 403; if (unauthorizedForFeatures) { return [] as KibanaFeature[]; } - fatalErrors.add(err); }) .then((retrievedFeatures) => { @@ -296,7 +295,6 @@ export const EditRolePage: FunctionComponent = ({ // We should keep the same mutable instance of Validator for every re-render since we'll // eventually enable validation after the first time user tries to save a role. const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); - const [formError, setFormError] = useState(null); const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); From e871e843655617ae75eb5bb688b42b331f1660e2 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Thu, 8 Apr 2021 12:50:31 -0600 Subject: [PATCH 131/131] [Security Solution] Fixes the `Read Less` button rendering below the fold (#96524) ## [Security Solution] Fixes the `Read Less` button rendering below the fold Fixes issue where the `Read Less` button in the Event Details flyout is rendered below the fold when an event's `message` field is too large, per the `Before` and `After` screenshots below: ### Before ![before](https://user-images.githubusercontent.com/4459398/113962310-aa463e80-97e4-11eb-93f9-f4a90bd250f6.png) _Before: The `Read Less` button is not visible in the `Event details` flyout above_ ### After ![after](https://user-images.githubusercontent.com/4459398/113962433-e9748f80-97e4-11eb-8f46-835eb12ea09d.png) _After: The `Read Less` button is visible in the `Event details` flyout above_ In the _After_ screenshot above, the long `message` is rendered in a vertically scrollable view that occupies ~ one third of the vertical height of the viewport. The `Read Less` button is visible below the message. ### Desk Testing Desk tested on a 16" MBP, and at larger desktop resolutions in: - Chrome `89.0.4389.114` - Firefox `87.0` - Safari `14.0.3` --- .../components/line_clamp/index.test.tsx | 163 ++++++++++++++++++ .../common/components/line_clamp/index.tsx | 19 +- 2 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx new file mode 100644 index 0000000000000..a1547940765c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { repeat } from 'lodash/fp'; +import React from 'react'; + +import { LineClamp } from '.'; + +describe('LineClamp', () => { + const message = repeat(1000, 'abcdefghij '); // 10 characters, with a trailing space + + describe('no overflow', () => { + test('it does NOT render the expanded line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the styled line clamp expanded when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the default line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the `Read More` button when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').exists()).toBe(false); + }); + }); + + describe('overflow', () => { + const clientHeight = 400; + const scrollHeight = clientHeight + 100; // scrollHeight is > clientHeight + + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, + value: clientHeight, + }); + + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, + value: scrollHeight, + }); + }); + + test('it does NOT render the expanded line clamp by default when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the styled line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the default line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the `Read More` button with the expected (default) text when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' + ); + }); + + describe('clicking the Read More button', () => { + test('it displays the `Read Less` button text after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read Less' + ); + }); + + test('it renders the expanded content after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first().text()).toBe(message); + }); + }); + + test('it renders the expanded content with a max-height of one third the view height when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'max-height', + '33vh' + ); + }); + + test('it automatically vertically scrolls the content when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'overflow-y', + 'auto' + ); + }); + + test('it does NOT render the styled line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the default line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it once again displays the `Read More` button text after the user clicks the `Read Less` when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 1st toggle + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 2nd toggle + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' // after the 2nd toggle, the button once-again says `Read More` + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx index d3bc4c2d50f98..896b0ec5fd8df 100644 --- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import * as i18n from './translations'; const LINE_CLAMP = 3; -const LINE_CLAMP_HEIGHT = 4.5; +const LINE_CLAMP_HEIGHT = 5.5; const StyledLineClamp = styled.div` display: -webkit-box; @@ -28,6 +28,13 @@ const ReadMore = styled(EuiButtonEmpty)` } `; +const ExpandedContent = styled.div` + max-height: 33vh; + overflow-wrap: break-word; + overflow-x: hidden; + overflow-y: auto; +`; + const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => { const [isOverflow, setIsOverflow] = useState(null); const [isExpanded, setIsExpanded] = useState(null); @@ -60,11 +67,15 @@ const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) return ( <> {isExpanded ? ( -

    {content}

    + +

    {content}

    +
    ) : isOverflow == null || isOverflow === true ? ( - {content} + + {content} + ) : ( - {content} + {content} )} {isOverflow && (

fIKVGvLrvqS|Pm_G$0!V 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 diff --git a/docs/user/security/images/tutorial-secure-access-example-1-role.png b/docs/user/security/images/tutorial-secure-access-example-1-role.png new file mode 100644 index 0000000000000000000000000000000000000000..53540da7170ea654b17fb290baa2a95d25764607 GIT binary patch literal 320953 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 l360EHx`iXf96 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�h(2}lV zUU-zj;V5hhU<<+?S33eQTVcYR9RvurEM~ntxdSq3seoatLYd{Bi!i@|lP+Te)|Os~yZ3*4{+&tF9#3 zUT2^UdmWjXsKvvgMvS!p_xU+54a`+s;aB&<`tW?$Z+ELr$0Sxg&0&ypE{m-hLx4^X zHI3rP#;XR&9g4t2!ZJ*Cr{Jn$$(`((3V)xtV$-@FeX#-RQq?jIv!i60YMmQ{wzn1; z7<{iNZ9j7JIh`=Eqq7qUR?*Umdi81*9WykK#GOl?5e5)5wLv1zQsP>u)GbdxBO`|# zC__W3hP#*3lw0Kwi)xf2EP>@BW$@vGg-W| zL$3(b{G}x^aiCU{-~K7z_4`lz$=2V$az(k^{m9X^kX=ED-rGCae*$#(?jdhAeV7;0 zY4j*r5;R<2K)J!g2#t!0I;FewET`D4vL&t5vnXz$V~@eJTyTCP3q|xR28|D& z#g2b!46Vm&$?Q0duRICmxME4D8*e;bk3%}OSK=F>pDe(p4ZiPda;xS=bg$K|4c~fd znWH-+2yB494Zg75)J6Xzp=_dq--ij400mOLa7VDOBL`%JJnF@&fGJ1}aDm;zTP zJJ+c>IDE4&B%1a6-DjCpE^4PU4oe}h^x}eR&6RL|qoc{Q@#c`zsYyorYp%yq-0@#jJiG@{7U-oEa09ByPhJXF6o`|RGJUQ;tcG9axawluLt07yBr&GuR|^6tk*ry( zX91LD8n#|iIba|B<{CGbn(_t-;=7B(wPH6meuCBs+`Udi(mezSYf&i^{u|7ncPbq8 z;uu`-A(9eF!i{9=5o2_*Cmu1+nRe9x)HNPL@@#tdXd_~Jjz8Ro#4uMy4cbYxHJ{QK z@q3ER+TtuLlF~1#CIW+C*1T43DR*u^Bh`jJS5)T=_!jc+f~1xic$LYU49+4%oz}iR z3@-h_9Yi07$JO9BgSBuz%K*H_^AI_fQP*Cns^9Wr8M*fdcW^rN-?@YHoG_uf`Ur+w z6ti3bLbBr#Gl6*ThA(w|i};Uni|}x%s(h{Q`JG%WP$9+8V;&Lcj%PoJZwvAZCGKV^ z{_70FA2%ya>%@Bsg;7M#Y0i{yW%YXcZmhBLjb_9)RNhw@-FaO0tpnnIKGHSg1h zmua;4@fVJ}V<*0DlR2XRm_Zqy1}RKSly7a6TbOuj{hrm<#X9%d3{S@%UGMlRvnCsl z@`ZqWFvqrHlc()I{z~004cxpm$UTa=s{B5l#ms-SF@Th6Flk|Ro#uwRhl1QWAp`Ur zLGM*d)8c{Q)Zs7>jnDSx9Lkg4DF4m=yue3pJ)^d5Qwo@oNb{Qa3i^xS0+NB!TP;|T z?usu92dThjY>b56Z>t+?90pjZcmcoF8s!>LM)CG;GGR8rotbI{wBnS4UN!i6MVFWg z39=k>`>a6&{JvuaBN?!A0udU?bFvfkCrcNxN+pZfs?WWl;dotb`KtgFJafYK4q-fS z{-XS5Ur=WXhCENh$%Ve}tRn5<9Mf)S{3cLC317vbqPgG2UX75^ zFar~rYBow9TKAmXo`*@8RVWxR?Rrv4IbN0YhsmasJgx?QPz}iDE$^dRwnv!mrCKz0 z66-n{T%^YD?!XQpL%Fr1ZJ~bUU29VMK}U%3kW}~F6ps$n3TgO*@&)c_?r?!hXk$6_ zkcfT{>4kSFfTLBx$r-#3KqI`*q^+=UpR5IwvZ3Nw`%c_WJ(b!b5SZA{@)0 zt&B~CcR>FBH}=pGaEd1uC)U7u2!+YhgmU$wzCqnc0RN^mnz2V7Ly@t6@C^Vo`qe#H zBX%is#!h}uqbFj^fvnj$N?8$sryB)IL2rs-F+DybHxDLgF zk)av&8EL}r^}gNbPfmbjAp?=~-GGGdG{6y7f&|A2HZIn?9r(kguZ#UmVLEI7i%);? zGer|zaS2COFN1x(<5?q|nXl4GhtkW?2%1#OZ6H^uRmimMie&y& zGRQ7ESg4IjJSs%fCUAdVgWF}sOI4X#R7t>AJi9+;%uzqce>-dhTil+BU@!qG-eIFJ zwCm^dN_@{&n*m)u3;>S6l)gY==e?~z49XDj%z&XF3gR-Er9a4K{!4mb zUP2=!4A>zH0jkDK0Z0>itIovk9!C1Dk+oHJU0~M7TMT(%N|(9BVvBH(R$Y$^Q<+BU zditw%u;C;CJlS1F=RtBvmGtjzdXt{=e~<(pokM!3@Q;~;c;uc#jb*h-?k{a>VIK-I zNdR2mz@yq^G%s|=U8!%CP0P>2rWvQ?+jnDw6^HTXp%Y}=Uhdym<}-!we*JWWbLt{> z4DZsx;T;Yq@Fd`OTOTTq+*vmD#x1O!UTFr9hnE0Fi7^yX3W{l7%gh+0Qab3n;cj!l z;X)@JT&I=y;G*<=nu8u_FmmI)#N=84p_*r-e$o16ptdZ%E!k<+ zNImWtd?smW5Pnzd>goJ~_e%U)l)cW)M0Pm;<5?0+P!D38Xo^oZ<&t&dm(f;Fx(gKf6 z-Q?vnM%bg)80H3j(XP20*Z{l~Xl8%3?)2^C4*BpId2*vw9$kheufVHo>Wub-S7O7| z3N}mc@u^;qf?s;5dgkGE^}E+I^~Ok=aVIJwCiiz_XD;;0x)ZumYrNM)Cwn&_b7_7( z?g&zi`@+6&H;G8L7rV%$c~*E~dv=fup%X4)PEaRkhw3v8AZ(-aG>D4#+2qzgbH-}a ziNUy|`~Nfx>cMoFdTKP^01f8tFWGAG&)cQjFT)eJBH22sr(Wt0E0kIf7uT!vFD)UaFZ};ACRuK0MKo1xnK8dvaI&u z9WD6oFI9m7C^aG}ULWbEd~;~lnNjcvw8|L;?^Xg-O=Za%n-`q9Qi{e*1-+?4Rj1N= zJP)z#nuY4a2gt=nMA!CYo>Ev}Hol3j8~(I6m<3rC+p|dpX6fmxyig$x_AkZl?>vby zBxrm$rdo5^ox^W<1S$A}jBXVKPzG0Qo!g^?oanMGJmpD=Uwi|Y;v7aaLgkoYDvv6x zRCl)O2-XAH1yoy22zGNpFe?N76#!YdZ0&zy-dI{F?&e|{Gku#p6^FU2Z#?c*KP`^( z{UEnsquC3W=aEmoGe_pOTLce_t>k9hpp?9}u_HIz2pExd3HW}!o}1|OA@!mZdnNha zs{O2n9G^;Vs)>-@Jgg52X$XKtBCYZIJKvhL?-n{UiR!UdMa4@`@I9?X?WVe|t0FkE zL#lG?6OAOb<@10cyGIK&a#8Y9*yqU*a4l5dU1rSRz~8Y@+2M~trh*=*bQ3eS7Rn=J zm#fzZ*DMLgYdqHsrFSNX;oPyecNepqti_pBl#eEX_bCxnYArN?Br2B`7$XAWmEW;? z10*IAvq_=y$wt-#(#pQ2&LcXSE4_gXX9?KKabN8}9|auK7adF=JH)(2RP06<8ANXw zr(QMUSaI3#86rG_#mzA_h137jbr^ryr3uMwISE^(Y~^)rd2{#iIG+(J0vw^lvJkD|5aIe(H%M?C;qw=Xa5 z?n1;n12#f?ts&IQ`&2;L&?exumQ2nM1?2DcYR|cH<|aq7X$9}I8EG~9RlVU#WD)2 zfu0m(9B1-=e)ar1o8(tkvVX(s?lLu_A=o~3E8qUve0cnQ)RiX9WGRkaWnYRV*yjE> zw)#lrZ|9v)WCM@Ao@q}kKo{0$#G0A3N#hDS2dJwxrDDRgd}Z(eVf3;QiN$N zNxgHU=lG}+e}$>1rUP1da!L>0KW723E6%ay{weX9uG=F`zjA|b?HK#|s}!%X_RDoe z(>pMsO8iS`Rxtn`@JTazQ~YoyNUq^xJqA(eA@o?$7~{R-)N~E&3wWnn08@T`Ryen< zc!gemz58Mj{Y`0{S3KxL{*!`45$@o_ER!{#=7buzJLq>kzzfFCH$U|{zSR8!YR_AM zbioU{92-09GcwAK_z|N=x$hJxj?+8PmD;tWcrSDwlGR0Bg~zVMf`phxSCKes87Vh5 zt49`Dvk9)wL&=^OP2L`}E6|4(qD)z^Nm5OR?;qL0PaM_jG$@Wy7m|LCnlZa2@Y?a> zQZJg0;iZjVH=#>^exaoZK1UB*InXUXwp#(J!&7<-=@%U?7*38~_>Kl~6hy-*&r(l2 zgm!s1nbckeZcN5H>DbLxb5FYmOR=|l$B%-%ngbydlDmF32!_OSq6$_VsQtz+Gjd>o zu>Z)EC`eOG21^~&@(QEMXh3&sK{G36#>Hp5H>ZHHYS$MMEHWfEz3(m<6V`*QeAgZd z$*`~cJqn67FhhSr?z{HaQEj=0d$Y(5C8#E-1j>A}FvmC5=vJ@e+(Z+c;N;#;hn*Y8 z7`qUbL$6LcZgATx=}r=Bm#0eRrI=jge)-J^T1RkJr4k(;6_zvL3 zolcFGLLN}jY2QT$&k59p&|i}?t{A%5=u2c9^-KzC6&ro$X068I4{gq&9!mShziP)N z0Gm2}0grJhyIecvO8`^%c0hK`egx_VsRsGaej9^+#6lr^l{?1lzD-O%_En-Vvojai zV(j|zgH25;EE6_}`==~-LvSB8#hMq{5hm#)qBppV|`25NiZ zR)~reW7hi|LyDmYcaZxM+RIRpTl@H$kk9&sW5f}rc_=er3~P-G0R&JS0MKn-4kR<{ zYi}^yor5Pu2Ml{uqn9QCU?W?uhA+B@fuxDs;(WIJH6|vhJEnmuYK0lIX3}5BtbyFJ0@BPu8V(5}%&%-{UjB|Y3{7L(GRr01t zIyBfWlwFjOC2li(hW>L)IIBH>`4ZxmWh?pYnWo?)p5fH+Mb{=+z-yP`F5cQPdi%{i zsZGDN?)I61!?_;gAx!%t=MvfO7MhaJ50>1lVzct1>yGoM1~W-7)rid!H+2sw-VLlZ z3+?fn4vt@~2q2}nYq{5@VC9|vtbSV)v9;QSxvLt$&* z4S&+2xReMKAGsFo7gYMXW}g=tbOJ?Lg`m*3FNXv=fT4Is%fp(+Xva^Vo87dLM+U$S zP=8SzZjJiEi2sUxa!c_srbiQYV|z=SaL?)m*Cs_-&P72Mvp(S!-Z2E;;$;YA1o6tO ztSR|?Z~Ev9D)acc7ufO!1BsQ~>7*&K=Np^oy5V;R4c6q?%y#^j;sY*yfvM$z{;ZoM zr>9;l`TEW~#UfTfVraJe&E~S4qr{h!DFpoi2Qbwq^T`Dc=f0IN{;@V$Z*PXRWmLo& z(Lo01OV8F)3IeP-C+F#OoRqQ*T|(eo-A({rXh9ORtFbihl5Q4v@2KmdIJiVJnX~x* zf^78TUQYkv-5c=cEQH-Lmb*iaZ@jDkax~JB$PN2?#=g3j%OtkMl?>kK_C`>$k(y14 z3#sFG`3y>CVPV_L06uv`H}l?`GWV(hBkjSGhsp%AT#izEbbeMR$K+nvs}G;ErFx6L z=68zZqMnSutQ9n5*ta(5>!Jj7Ik{>Rgc_!$>c!W%*s(_?%#G8$YIly;wKFBv$RGK1 z!tLAbl3pUlQ~>Fv#RTM&`Oiv|j(UjQmV5Wy{D7YxE-%jD6zO3u*CVJyIsO$B0Y@{Z1<*7^}9jlUtFybpRPq zO5IWyEo!noBrDYUU+UDf2L81y08VyT*3-z==9uhvBCqh`8{3sHSmwm3d!0j5uj*`X zC{#wsr^GwIP&y>Q}13hJw1}O`MQWazeTnRpFDfX8ku9z8rAlybcU|qOxBLVFy$5}-E&`hm@ z2=b}+eUdj?Ork>wm907L-c{I z4LP9(DwpdGjF9JY(NF`iEYK_76+!Z zg&sMHD&bV16tPt?HmuuPkgKy)si<4T6(SvC+}z7lQ`@@|6kMrzFn>CHrsS<{-;DuM z8oB0A{imBHb91+5tVSw($B}EKH-0XO6q)5u69i(!xn8DmIq!ITmVo;+L;I-o)u^gd z>C(WKy(CiK3_n5)aIOaf->DA;VT<2faPNAEbujzc(dqEx=-gwM0L9AsZFLl)^gDVu zq@9v0qqv$33f~Z;#9gpg-fj>!w1`4K};} z{-xaGZZk|6dYjjFXR{&H9iHyptz0^2NY=}<))VqNc`Tv;ce&24Z^2ZzhAW9?qP(*? zt(ym#J~NdsY+UW~*2lFV5|+a}GjNX`LUD>Xn(?({vy+tVQ->uZZ{>MGXN zt{W4ctaRP9SZ=SLxuPx`lcvMd1#!uk7|)7N^&^=gJ7Yo2A!4f?f9~6z{n9TK5skqm zoDwfC%t#)0UX95;n8_ka(_pgNt}KAYyXN%~cFrwiBdS6E_Z5Af_FeT#LPDy+S73Gf zjU{g1m0-P2j&qAPoUr%BoUOn?QuVk_&{rQLRU|Dm$?vo!3pyuO;knr6+p3Oqa+GND(&j$0rFy z6;zWYz-;jmb=ulv%mV6zwLKiPZ0rcx{*&bg0bdGNaUy9Z#d}Pn7o9pD2a%DCln2{ysYjfDE;?9m_U`S?LPCOD> zm-cw!xlh1sWd89P{TRumY$?Ro_ee`oNcM1zw`YU)If31_wOoa=w?b0p;kUoK@|xDh z#9TL<(AOInju`WL7|NnGV2$*gM@ib9E54JX_wLI3$vQ9fz?XC@^U?UUYruC#xoD2} z9p{iH*V)l!pVpT7ZK_%4M=*J-bBQT=)D+0CF3*_xd=hNsx63^;7#A`&_CQ|UjNH|e`?M#j z7gYu=lHd+6U+~y(|LaTyw@44<5iGY(4KNzloSpKhAZa|6MfnkGi>XjBVhCXl?tN7^ z(Vw|vxW0Imxu$5qwCB3rd`K&xm!#mlm>@p1oN6(#jR7UAf}v^A67t)%E;zY<#9JtY znsdXjVkm%qLh_S-h1pG>tiBH+Z^q5X+b=FJ7nMZrPkz{~T+)vmI9K7bo^-L}NlRk` z*(q3g(yVjuLX42P)=&dE@Ifj)SnOMJ;s;ZpH0PU#CU_j<5LSV+nbe1n+zVRC7N2hL zJ}$EuQFM$nRJ4}@!cX>#o!2kq!{STjzi}*w3``K}NejI(SCo#1HiWqHX8~UNM3y9% zwS#?Ypj6ovaFG}QSlr67Bp(DXue3&UkM?Ts%8~i~(}#;vf*Be7$m>6u-*HD6C_1b6 z>0)c#*CB;9N_3B4Wc4i0~VF-`%|-7g9NeK-Dj`;AeB$OT7`ff)0!$Vk{+s_Ge%boOkV zZldozsTUEs!3@U?E}LLXCWobC_LD{dhO%TH|bOy|tptItK62N2&+4Dk~Et~B^zt~#FAR`ij|q-a${ zU)`(pZ&ve}i^c49sIu9428EjSX?R0L@B*jL3$kvh5W0K~d}U*dn!A`j)_@N3X#463 zpSGj6?5!<6w{%c9K&lyyF#cI7T=&w(vbB+0G30%jtq02Bv&r3D16fL!tB14?0=iekSWR5|sE< z(wdpqo`2@mWLkTyOQV?S#nt^+llbbH@DCrZrhRxxr^D|UEIl*~8^5=&QhI=a!+Gj1 zP8qNvnDEdJqRz9{I#zrGl(o=Bw)k)WuVi5PF?muMvewWaWK^8D(|ia)H3D zpoIZGS!vBhen_J~V&H=_$q2ISmfhET8psJ=y@~czQvs9-R!luqrR&U!sK?lMU(I$W zV;a5p6F1iUebQ|?A+anD>jv4CQ*wCsUXUP|bB)ytp?`nisH#-$bH1uaf3r=;t^9<7GvOaNPSws@-2H;--eIxzPU`MkLs-o zBB4s&xoPCC%AU$1(F>x@hbu(=Sue0XA3XYG_kw3vs)(jqLI;?YiDEXOW;3S$ID+%S z?oJ={Na@|Lej7I18x!e<*;A2Jxg}@58;vN753+FfNVoBC_s7h=2OK9VJc}k#FAHqq zdiRPQ89%%CG>pi(G^~9uZL}+L8B^_%-vw{G;tpiSGFOFV>jw|gwlZW=Ce`eDz?Tyn z(t;>=rGmF?^u9p)XkA*4i7V3Vbx5(%at#KsOKIp6pz7}1uYLoLoq5+PVu5AY- zlXKr9Che=k-tTs;Ee-36dK6i<4MwQbf<35gOWXU!5YAi%;X_i$~kiUkb{grOBO!Rk_9=ZQb7CQhx9d2UqWl_ zM^d_xxN!#?>(1EtD6Y>2pWiG9v=)JKy*1fB_K80=tK$P@>?{JsWd9ntz#R$aiEG|_ zrOD>jD+yVS?fEQFvFY%j1F!6;Dy!6!k|t+4nQ}IlDX&5N3#PX9K{lnD3D<(47aW*N z54@>c(}3~KrV3?Y?1F5Zl~NTo@xv@o&ZJzm7+c^?mHYMkn>gGa{o*NL)h#i~=)0Ap$ zhyY5yn z@g|ka^cbu2>(wD$ZShagKyjxz&4KjC7-!~f0Z)-a|M}ItQh*uDh;mS>)72`#JB#D` zrB{1{=wD1gZ>7!xcS=c!R}Kq6)--_@3IcD!{O9RP;o#Ld5HsQ+`rMaY+2!L*iIc8_ ztAKblWCUHZ`mq%nCBfZbjlug=ag>c~=@|E7L51fW&~hAX&uh~G1xghO_*ud8j|rQ( zUYwvh_7cxhc9jNdZPDCtkHpx{F6CLxj`OzwA(JP32H2TLB}1ya3qR!c$PL&}YM*%>C@63V1{Bk@ zy4jRNQh1Z(b@0dIO?<10O=>#UiqKz@#2lX%HScW?_9B9D-Z;t$w~6ORx+k`5eKW;D zf-!>{x96^(-s~$BV|@=e0h`n!Py@joeb8jHkTN->&ozehhT0{U#kaoO(8iE6$k}?G z?kj%Cu>`r(Ac)U0FHh$%$e2dajw{dQNS6j;tP;%Bz~Sc#-&HnRwDZTdo85i_qc%Xu{8w406k8mXKGJ`cV8FNiVtS6ln~TXmq-$y z72^lkX=!=hZW4dYz}(Hn&OEArek)PdHqCUEzWb>ptxw@k~y*#wv3n@&N~e;Hho465w*YG&`6dWq^2zKG3|TQF8NA+|8g2gA9chh z#J;>b(GY2H(~9L-(h?QcD{rtoEc?)FZGv~#^!wTbX|FPv(SxoRS2#DC8IserLv%0E z%dLl2F$&r70)?eP#z9j4mZoy*FB=E^|(BkQ8a6X$zq< zi{08*eHy*2oeTXGvtB^%y{+#gmB2%p^eot+_&$OqZL#9LjR~P-a_{4&*mzT~H$#%s zw>xui2LREb;{6&w=WSiLiXB6%pMcCGi)l`@H;V+}E(6gx$if0$cy?akoGNLmbtKX(4SWI{GH7D#-V>~XAw^hC2HMC#^C zjopAxeuM%z!vx%fy;y{MwLL_->-nVp@ zBD|ZkknNTehAlj1PFyflRQ&Wp+f|uzVJjd%)V|g5Gj5iyTeR!ZZlsQ|f)|v0B5=YYw?NL;i$0d zSH=oHF|UeB4mjS#gsiZxUai$C5_L#6E`6C=!Wi<+BUM`Tfxi{eAz-}c-jVK2ql(>s z?A?qkyJ*LOZ~64vVw=7Tld=Ppdb~dqkE5Na$++!g&ri53mso+a_uNNaY_)yS%4nql zB2n!DFovew01(6|OuD0V@pMIdSD$>O5ZS#3W|d0@&X$K^XZh0-6|L-@f%=k$XWEKv zFTi`4OT64P*8m7O7ZDB%Az=0yUha9}h_5=X$&KDk+#i4-Wi{_|2vAX1Df3O#HyY^E zg9b$GoQ<1EAB~a7Q{O+JJ<{|XQoR+v)QI){U9%R%xU4Ok#9yj$gmy+F%7B zL2R>))*WDssy_01i>)3!q|YC(=?@Nr2)c}`@}N(GK!yakZoMA0C?qUIR(gP5tv*bp z_F>UQ&j!3WuXAB^6$%lZ3em<0if7Lf{4ErU#_cSCcxEYA5IK2+rWMqftCYu5!Vr@5 zvegZDqPIh#(O?M3^1LOM@c zd9qMre$@oT9HXKOx~X-@TnY=7*9|+P%JeKZdnJ-vQ`h*kLyk04Kb^8ZG|%q>1*pkP za~dK88ZPM6!WouhL;}Nx2w7{gzB^EgYwxwV4I$Ir*ki2XpG(u{Ort|A;cTf0Z4cuE zzT+aklNHuQ_QYKT=+F|KevpEQYzU$UY`^&!B~a`)5x0;EYU1jLTX>KLt%*ZWb&%nr z-q`y5VsB?NOI1{-=3MracO$zf``$f>`@4emm2WJH0a`@qd-6?2dN-?4-r<`)r3j7^ z)csBB#K$OPX(P4D_kI250RKGMuC{J00AfE^v+uc-!rhglN7$4$P_8}~gKY?Qd{|5_ z26SEotMg%k9ZVdhRRVge>r2oZZuqy`wT-U?EH)G_JS4RNP={E@o#9ahH)A+rmEf|e z`mZ~oLMRZLmG|`rEU~3RlYhnum-h5{Iqp?PnDVixFK)+O%w^+~oJChBkFxOH0FI29 zmvoH2$%A0@gSPo@<7|_i3h{)Pwb_p?;Q30F^Q2UNhbmW>Y|D%xeUOdWAGR|sniXzn zbDjFf_+-1S(a8|@IQi-h!aZ zORo=wy}{%14W7PEOmO?~>M#bu0`KhU;KYeSLtkdc;XM$0>RuWA_Z|4YH7JIera(*{uQ`!zk1?Pfo&dMjS;;ZcK?1@QY;@9;{oD9ii z?#k97Xb_-{l7L7|*2H(#>ty)*=vE2tS|H>^C|YK~Adgw0NSwY5IxMOGm@FymY>`zj zR(6=n3_5Ms!_l4cSU#dZ1d#74 zn(2?!zc9Qkf&yUo4EFH_mw|$Rm|EcV{l!$p`V?^nx+`?uQIS^)jfd}aD8<7!_&b?= zcE6XQxzf&5#v?C!qYc-)ela;v@w)AA^j^N11D&r5zpwmyQW1K%dH*5 zKv@N35Vpcl+-2-~oi`zT`6k`bGci#)YKc<@?fLVqQmW(Dan~=_TzOF3Y70616qxxI z=hJ|}J=eX+xd}(j1hc?n#;PJj^!)kSak*b7#zhuJyh^dJ$}R7A^edy+vFk}pK-g1p zAXf1gRQ;|wl00=8x=lp8?MN2@h{UBY3{d#^#*@ z_q(ee3+1=GfORNmcRTzOkEap5S7;9{TaqYOs4t;DTHg7p)9cMj@8;TQWe@}Mtl*;u zVP^!d@2t;trn$R5I{fw8O^c~$VF>l0AKV+8cEe{qf%)cDzCaSz`1)yXtvCW?TNUAg zTO+tnJZo+HUKJ5rRO-wh%dfYIsf+|4j5mD-bGo(qL0g(=wS*ykT9H1f{F4^<4|?9E zXxc+F#7z#82sk0Y9pfhbw#MR;{79>(P2*RmMWWmLEwmczeUyx?&fVNUd*ktoGA%E8 z*YJpsD2*u~?y)8#&J;WNhO@DDPf3M!(X7Sr1ep|%9^OApC#BYoEZ4rO(kH62{e_esUSLOE} z@&9;_U>MlnTF_&VI-U3H;(LMnNq1I_^xQV%04(NfD9|HLmt^{Hl<|KXJiAYmF9{5- zh3p4F-q)>^#!xDHHAYb`xKDaE_5^Ih7DcYoj5LmPDfK}U{kkAzpcZ)a@`g{)e`Tfr z+gAXRUa5ba?ap+6dhVBR>(5J(a3JQ6+Uj?o`AMVjUn|3RJb;USf6?!yqZ!(7W1#uy zZ$J96Q~frE-^TFI)91G_{Qt%nnuexy|JDNd=llP$BmAd-ejCO=4TDM-5r9bpS4diy zI^k*zywCa@uXV5f zA0gm>`Z2N|h}PV`jXHjGnw+1kuqUTos6VRp9=L_si-9^y8|pPtbbqGIr75?>{z83w zPx(iyAZVf*ge=BZzwNM=ob9zgdFK-8s9jE_S`MSy(o;G&f~M=-uLP8{!nE@1w~xc1 zQ2~g{SnMI&`5553j}YZ_|ALn~C5N9>?)RLY)B1%{F0~tY%u{L9w z{|MaAzcZ(p@3h4UhEM!LTnrSFf%5%9&bAU+@+?w}7v8CnE}3^1_#DIwM88bM)04aj z@&pL^evR~hDJuUnZF8e3f*<7kSjZ%8J-8*yAafP?tOJLsVJLcOGg0hZ>~ShixMfdQd|nQGorN6%Z*_u|DImutnb$efnRXDOxkCt_)o@7 zu9I?28Kl^M}jo&*)zAdZoaf>-=&*+OuPDz zinN!1=a<)Dzzt=CHz#iY!uvf4xF**hhT4FznL=L*{ei<#`hfin$oV>7`1xDCbb#hR zFN*VkPSGVh+VZIa!z*vtbIMAD|EEp--~TW258x>o?hNbF3_}#KtH5kvnn@fyB8~kQ zbV^lT(3brYo|k6hNrQl856~=ZOu6^{FYIvF?$Ep}l6VHV|HIwDOS@!GGxXK0g_d7< z=cj1RoC9sXM}hl)IS;ti|0tAa07fUo{b3~wsLuU=^mT6|4|mH^17iKjWT_o6@&0IB z#dzZ~QLproQP^2t3-gFlGp%I~VEyc(eRlL1mG!vx1jcM2op#>_cZvZD_~bKCPR%zs2)~_zOMYSsR+Q`T9zp6-`EY zB-U?l#XmgK_3{FnFn(bEQGs@FP#y^{U)K^44m64yxn>ksYbtG{YSIRq1HU!p93-B?sBX^V%|94Ai;-6-LX5 z>+DENTgt)v*0W+hDDNW=#hA%_sEeZa^u6 z)hf2q0N8PPeAZ__e5Tt_2jYvbZavPu-O=XyRLclYU3_%59)&GYZjIl)@q;R30Gy1L zFA3YU!(1w~T$HOV4d+F;65hJmnOP(deL9Na;^B)SR-|2B7V)1W<&FAr7f_!cFNFj+p&U%`vR}MSeBmY9V&o0#pW-xNwNvw4`$@CLo?o z`Qgt9Z9D6O>rB*%g*7>CzKc3t5a{b*7;jxRC2Md`muY!I!{L$S7YKBP(VSYV?;ByZ zDDtJK(ENUD!}sq<>xP2t+?gJA?PzWuj%o8lj30oFNeS`f7p6h|aj+clp0|28J1D15 ziViM6l90cpyD2tyD~FKdwamq=73Rglt6%-%-Qw6lJ%D6ANS2+I5jtZJ9d5-dFA78Yg=u{c@Tt2Pt7Tq{I_X zWKVOy^R)6X0l*lix$WY_B(`Yb%D=`T|LMm|Y=AfRUvGY*b>*eaX$^EKMuS+|^$vj3 zZ*Cii%j%DbOACtRbpTYaeG?&){JvQd_y~65kpqW5clT~rNn3k*VtZdZ22~oXY3$um zpllF#C3xL-Yuz$UyYGhOXU%c1jwqB5^Z}Iw^h)dz!LDzOmZ{(N-U-dFMg(ZOp9eiF zfC*Z1NCIpk;@C{Nyh7#k>n;1|0ThUhTHg&v=at}pKmPtx-2LK?D6LHTFsvzM<#-Z! zkHW`#bFE3k+#dK=-^_~!Vv)P8`t6&NS(8slX$@|ZFM>dfo|EZG+(Q!&9QxZsGRRuzE%qqsgO~cOdT?&m8 zx*QtE|0Lp;%`irIE@%iZ)eYCH$H5KW+1R}ud~fVMQV`5IA78=scDv604-I~^rRz_7 z4CX%)oyGA1H}$ZHgw@#2b4Zd-y@~<^rNRg)8NC64;rPZiPCO(5JTqU;M24i*(K_t_ zqjH5|(1f70hI+d&SJ=o*jF72lw>@UBQq!X`d~cprPbT|ul4R}Exmp40?x}e>wR8Nl zs0eu3Cz;+DQ@@pY9h*ihN?-4xEoPCG-4XOkVUoOtFp!$FH36?JL z{s-etT`xiwNAT2|3Nxjz9GtCv@>4rCaSAJzM%bi`|Z4Z#%j1L2Ey8$?ngegtt!r+GT z!`(&_`MeTvsat~!@4P~_1Gd$7QMDsl7>iML5+d$+1jn&7Vm;zRicXQvm!?(GC;8L5 z^6@0pwn%r1ejX;WC>aw+HupD=OZ;E#y?0bo+qyTrw+aeKQKU%Kt$;%4p;tvg=}HZu zmk>Hgui1crihzQ&(4~`5Lr01tAcRn*h9V%nx6t8RoOAcN=iYPPW5)O2JI4EmLx#B4 zn)9i@r_5(Mzo#mEmlN9;WYHJIZUy|gYh`V_(26&%(tTXBcpPKdW!-3fA5O?2%dNM6 zXe${B{+!GuDYi$&ORGlleKUuyAd)r`eQU40kjuXf6b)GVneI(`$A*%kWM5nN_827# z*$jnXLxo&W=@i&A+{f6Tu=!$+EDT$5l6#Ou>h4HiDf4qh0}z#a1l~v z>{bR}z%oZ6=Lz}V0TYaVcCPuxHRXGQ4Yl2RdSf1&?S0c9mTtR#srUX7vNv02wY-vp zf_|r`+>1yigfw>HmDV>8T{Yghs!R1jXB$bQp4F(j+$0QMKfAfyFymuvId8J{<1RvO zUASs%o%h9&Fy>^}s5p1By&wA^)`*{4MYt#s2ohqaL@G4(VoK_*R8DPFci8alm!! z47?XTn-dddt;?mPWv474$2f8+qhK?pB_=`l{7)7SH6)paBn;OYKBXE_oo3P%rTWdp zlssT^TDz~L7hQ2?A3tj+hS!-2f-(KkFRz}JF;yTb!o5G=uKE}Q2)YuhvEmkzCQ(-? z9Q-Auyqp|bb#2>n02txtL0hGqtWM4VPTD(Q7y9~gppRLETISKAhjf4Ot(@30@T=L7 z?HxAKF}FDO(0-a94jQ=Yg`&9%&2H2th|orvde@rUrLL+?j#ER}lkJ!I^$Cw$>|z@$ zQ(3!=B7j@A<*X;N-Uh)Wi!No!f+j(6eoN~%;u8QF92 z|J&WV37BUOx}R2f)~UZw0COiU9kQ#OZMETW8Rcw67+b_qle!%u$4^u6qQyM+!@iXZBx`Dt_H!wfD4?BYjDVm}8xz^tp4ET7I#Qb>gA zvPtDDK893!#DET5+`ozX$FM#SUcXC+y%D3;DFeqUQ_}h9l)2TBW-7YNO;Y=;iakyL zeMX`Ak!0?Kpib7K(xA%GVI<{csGQF{a4sS4v`q2v({W%Ggi6doiFr^~yCXp~eDe6T zioK+t%x8VC((Z>|NuTly4YkAXlT|87TG^2C0>a7HJ=GRbt=l-}JGGS`$juC#eemOY zVt40?hMd2f0jTV{mP{wb6K{N4!->7bmvV|-#ViYWOKGzz^m~1d!O-fQkNYF?m$>}} zJ#VvChKbu&4~xScplXN^i2lwl`AzVq1!7?Mh<^jBtSDKeu3r2R0_uJgCF4O5%OP2f zs$HO_#M`KhIGyQpnIxq}{Q!1iEIdHqOepZ35br%!P4?4%jQ`@+u7OgB?48gS>6f?o zgY*IB(LGnHUqXrB0xkcBo4L})9|((;Pft8&*RooAximYjlW2T8dg+Z%gaM6*9Dg3Y zhs9N%c_g>N=+?6^(yZ1ALRM73x zGR~cxLJ1YgLa7RlpFf7=HmUO+)P0S7L(gQrt`_DEHfziB7*zi$*9LV)RDK#vtX)mx zmCBlFx;X)Msq*GYOW1$yXUbi)2sgdpGhQq`j=v$;ltT-&;F|k$GIxqh{xHOh%9yQ; zRp!d^PTty!OkOw1t!K-1hY9>SQk!9uffp(dS;IK-y_Zlq-H6&88=nt3K0aWc{se@3 zUFj@D-J7NaTzwY&J|!Gz;>=wp4Gdb{8zUqdJk77Nsac3zk1M1dpJb*s?0=#z;vf`! zM_&g^iCh&7ti<|iYU2nnSy9d|tGYh2d3vXHM9E z4tO?JV3{_2ao|!?aW^wFJZ#v&fggD`L~axJLfTxNoLi?TJ)sVaMERHYb_lvRZM4W zLUdrvO}kKAIdx%dtk`^hwOCG$iF3yw(->jQT9fyT*U7cA9Vjbsy^nUwmhF>;c9lsJ)o)@vK+d`o7AWRbW$h)3G`uedP)=+mm{TI)N{# zaKKSesPjpR2;WOdJXq#|EW!-|v+v++iXV0U6 zIj+V-2Z0KqK{)N`3#AQ~waU)&&K-iQf;p3$5BC>q0ex>3RDP^nPintRsP5N(W&8Jv zKEOd(-5edPTi$y;Z;eTWqk$KJHD=U9yNaaQevx(PM0MOdKY!Hox$QuMep>t2y#Rr3 zZjnFp20D!eqj;?@|2l;VoV?LF-b=bg_%(-+n+p*Je(3#jS?>Z4ZJ0EiDw zm?0+X1PpTp(gh~~+S~PW3G@SoD(2l0uKHQg;HJReyJqrVF27ej%4gzdh#a!=-Q@mu zT4QeZ`C)uS@j&$0pZZ~O?mxeTUPf`$&~-TlXM?NiUrtm}@V2IR}-dQ#cQU~7OO8d(k9PF z?|I5e4VeH3G)WUnMn7IO6G1$@LE$7l#;F#`;Sl7rcE`>ok@YVd~py%7ayMl2nP5`G_7L2TW4g#QVO* zpx}Wnw}(D$5jTKHDRGB=FT-o#XCK9_rQMpLAEdaVrM)k0?40Of*&FF`1}#&vo;%uRWWfMDDVkJa_`Q#^Cp>`H69%Q;rXokL9=*4{HGeRI*ljz~<%tyPd*w0Hc5Ed<633A-q~-;F~3 zAu}qn!N-L|L3z6=kR)_7ALlZ{zRgsD!%IX0P-MuK?&v#2pXbw1PSrusp`bD=LYfx^@;bt^? zSE)bkHym%w`yf*Iz`_IHY9Hk((hCW0v;q<;>p5ISQL!lhiQOb9k1obFlSO0{)TVeeC%(l%EkKavdnwhEmc+l6w`bC}8;O z6bWvp9@?wg>?I#EBNa^L!<$Tq6Pw|W)StbPU4NBLUkzMf&|$@-W8 z=264xiPNHpZD6B=_TKjFyXzsVzJH@9xWp@1tA1q>-yIiX!XGa-arQZNSy{`y&>^9~ z4g$#Cp{lG{?CBx$4Ps)!n!9j-O532{K?^7B+wKhm%u<)xdPmtn(#mav!Q$H~25CQ8 z`Pxj#khA-Deq10TV{_VkUW?dq1~Oks0_#%V4ySvGwV7yA+#eyYi}7}pBP7}D2ZO?vd3!Ns83zA|H5>?5gzCb?$SrX?fV z@-1L=Wk0uJyyQR^lkF^;X9>TgI^pN=69j-B8xU)G?xEYKS+az>k|2PvD7+-T@QPw z7_?O-;|`_h70v!!`dOO_TFEKmm8COzZa?L$fSkySqkhcE-ni;+>Ep?w4=t1ElI5Km z;07PV`?})g138Tf2_#~7bCN1fGAje!!ekqjjEz?GkUY=tHNIW;elOP|qlgBMRxS*W zOnw6l+~|zmkIiw8Ml6yNzjBn}nS2Mx1ewK;Bg(FyTjop9>;mi$DD(-3)@vN|XV(QE zF?bHIRYe7wit-j!Lxnd|H@{X*PH$N>cj2SV@2aKre&MLo$b6&&mOj@XArL%B1F zFCOl0mAUR2*OYZ9O@NOMe^gjM8AsCDAAOO@!Oe6=A=I?20JJzVlh~o5qDFGh$faKfnOTiaOkL{68`r_iGN*a%yL_ZMFK)-t8OAFb+Zaw4U?gecR(2+v323F8aYBOJ{l ziMZzOUFY94YUezc3{7+z<&d2zkLX+=fRnSE9Rw44Wo1;~wh5%}4z4AjLscQVN$bNdge^+r-LdpQPR8xN8yUxkPmIl7o z3@FFUzCJyALg9F%nzC(B9G(q0LyyDdoPvRB&&G&yRJmk*sNYeWWFwQF06a%3;KX;d zz-sMxJ2szJ--XU>HWb?$@56HnpH%E-GatGX8^ueR=<9_h(PvEn5xLvRd`L~EQk zv-htU%I0%iqC*lJkDjPhm)uL;*rPPEP459rx$-DYzxnfvDmN*`?hfW`_HOm=@tn-O z#1Bz6dgI3}zVZOGK%Wo)6;G)%Zy^D{#&(yd`>RaNpcQXK;atq0P69?0#T2pe2GKVU+ zV6;q9>*aS8avan~IA|x29_$#dA5UMPkoI0zolF+OP+9X^4?;FV9b(I5PP$`SuQExe@pZIvs0GV)5OVp z59mm8-1Y!U!O%Ah_wUWBFvR+KZx7?DWPr$dBLvl%vuZJUup>rmdoq$X&#Gs_FS8A= z=_yC@LdxIONkZevkXAhWm;YDge_zhmlZ{IWGEaS+nELb~-<7-a;y+1$e?U_6Ud8d9 z0?l`K7o#Csnj1!&55LB0on)lH`s2-y(l?hc1TlQP7nXkS!kdzyH{bjfZne907pE@e zr!LvmTIb{nw|~N_6)btD`lJ?kw(X{MFCWp|lK=BJpr7H&KYmbs@%sY|-^s-Vj~-Bf zE*DQygdPXRaU(vu!#GNA+s_O3`J;f;NUKvz))#FJ2^HawvuOq;0^@uyIzBwqx9aLt zzGeL=29)s`k`CXTey(qGFx#iu3peH<9b)s}swr`H5kw2v86;GF(G;)&Z%51}j;4>G zVoFS;%}J`l*bt)9jd^nZ#UsKwk+8$;wZ!+woV|1G)UrAUtrfaPvLLaFhBsrFZvGsbmlLJvp_`qvEjY{=T;1Pq8@g~UC1lHq{QtR<-!EX(0oxKE@SWo(7>h7RJA2?|sKb2M5JF_>yyOVq zAMNG7DF-}v1IzNw#s_q-BynWW(sgdx7I8)YnLaWc5K&FCVjayLCN_B>k9|!w=^nFz z92X@E`Erkdbdd0!zkCJhKJoqSKS|&JUcldeb&D48-gjFMq@M-BW{wkcxCGWNX)HM-YIqa)w*5yw& z84&Arx`g9AwOWR&zhPMmM<@8-M8Q!IYx ztAlI&phAzm&B^R1vHzh&|N24@<%J*^FbL!luJ)3{%?p<}3jqpcohro?CEH8-x$L`Y)JgL<6k)Cqx~1H z{~rsMX8?4e_D`00;Q8z2R##{e%7Mkl5OVjXe!;39l5^v6tBvFTZS&u=_5V(8u2rv( z2nh6Kw}2G>1O$+?r=Rvqt6gWZrOB!*(AV`J?V0@ExiDG!51mjvL_F(5tBjqCVl>&# zF5xAmUXoyY!{|KFS@s`gIdF{(uoUfw>`~I}ss0Va*#m!DNIQpsYt2N-;HO#6A*CaO zfA(VkZc#mt&xO-WZfj?HVXJDwf0jx02EHE{dW)P|) zg0b4?BG~-mJpvET8FZ{5y=jlAKHCg2G9GvVJfbg3q6>8~ND#(2jU0=cJpL73INt#V z?fSkTo4=2X*FyVa6;mq}fDWc3*YvGI^3B#(?qjRLR`8g9YNL* zk_`xJ$<7tDKqh}DG(b6FR8QZ>cr@vk?6Q)V@3Fsd{7TT7u;bdYvGT=SzqD3z z2%Hy?z|n74e(~PFbV}705c{Vm+ecnRK2Q$$Jv#@>OTVD;U&Q9DIQL|y2maDTK43s` z1z8~~6!brF=U=b;7q1)x;2ZnrXUDRsL_SamNRR>Z%?H1d*uQ@%@F_q}(R@Szy9SXD z3eV1#D;`)n$|M^LIJAhw4hiE!w(s}*(9QH6n^Gbcae?~6X-J$yS8ZL)$R|0{ct2Q3D)uK^i)QvQ~MI1)A8y#dhlrChBBCy@_4xfF*>hm08uk0`ssmpcOm z5CgGF)8-%|MfS>7a_Nm9%^8{tWPhjC%ZE-*3r|nA?%n9SR^RS_y6|)~W~%-DY3#ak zD!VP#Wd8Cl$iR^0o7i__>aT9YKi)TXjXu2ko*#p!ffK=4vD!Iz0#G~frUgy8guV)N3;!9?taoMD!Zb2%$B)MtA5_?d6o?l}qHN|fX%Ih3FYvaeIKV>`@! z8;Y-kneEvb?{VD5>?`%4?^jY0Yu9eSqeku$TAd*hru4z)6%R1z!sPqRvihQCK%vcf zaMDD(7af7P8cL+*rN5WIirn4h(CLi+b3}*<%={z>b`k`O2=I3xk{WP@bQbWk3M!tL z7HGS~jff&$@SWB5FhMe8VeL|fgEQl=0sO*NO0%G`r57c8=!I;2 zbGs~|xg!j_HRMv8kq*%qUn%8Be4nuPmkFn521OVNcyLi~`|G^bf;-$DnH=#|bq&I2(=DdqfHd%@Yzf|CQI^FuIGDf?=^b zVC)ww+CSj*ljW5K5T?7~tHGbkcI?baFj+njdZF-n`{m9R9|Ahjto7~U?9?fdw&=G& zGm=!J4MVTFL~GO+tAliigdcrXGt208441BWIIn|!3`C04H}RTN%1!pS4@!^UkGHvc zq`WPlK*pjHWBUlt^Aj$0E1PB0>0Rm1QNwD?BZ+09?In6XLq;~=3u{YEBzv=RhdmE4_WR@%_euwzISOG$#8-gvP9GLGdzs&%GAwtN(T4=urF zm#iV#n2dK2-2wZ1Ui8>1Ttb6kLgiSw07sxRk&0<5L;+GK_vZBrFw@@{+iqWb-}2ek zYSt8@ivv+4Sp}ADNfb~51^+6XDwMNxFtHtfl?2yu8)JDHr^$4$Wlt5AXhqkxHGnJo z%>eC89~Efnm#;k$Dol~jQ5gHfKr&BSC{le;^(+$6a<<&mn+TEI_#=ln8*ciV>_fAG z1w=OG_5shxccHs=hMY?FH<4oOS5*`&1h2*&w+C5nzD5T@Dj&n7+vGO$Y9lGKqUPt) zUI7+jTH$lamb*PR3|n^U)%67?_&`_r^y{CHQhloI?31VD#iCmxn%tZgGib_4MoqiH zA=CYD$X18x_w}K@|F9JQ@rSp;O`77Um>7oX^4XV=@$%B*+#cvaYDG&AMB2xf;^`WQ zEKcVFTmfnor(i4iLG*@Xc(-LY?4*>J4dQ!HblT9H&@uctJiM7R&-@3f_q|hbtvac- z4vu@Z{YGg4<7dn#VM-0mCgdX<#~^zIQ}$ui)~hdyGnm&C@-peG7ThFJeU&c*;g*c;G=y~{csN!RTO-p6! zHM#`%b(9Mdvh{*jOKTwquV$v*W#!}QB4>CkFiIV+R1*?J`f$_NSF)~^z34-i1?VP% zM1Ly4N8EK&K!mo^MgJF(XZOD@!Gft_ff&VZt0~NAkN&AFf;G1@GG`U&1|jBXf~5udKbNP-;)}2;M2D`@6O+TJMR>~hMpE6%aeI{!oHD<1eS>luv1~V!>t@_V#|=fL6VCrkq}9ZtC>25$yTEs zg(G7k3I!MJ-Ti0iz~K~Dx66({Ry-(UyoY^gwJ0%8?QNR@RfxvK6H=~$;F%|Cq|K;-3_#8({tsKzn;}oE1nrl7{F_bQpHi? zXxUMenJteX4*KX%j`X6awLW#O(Wm7U`Wpq!!iX%RCbj=QOBB&b_cJtfyaYQOd9z)*Gwk7_h z%UryWS0E`drW*>zA&jzElAf|3dLCU)RX6b+3YhpOYcZ?h{Sr)|cewW{%vYbzVdo0g zS8oDNRQ+bKna)(nm?XrcL9WixOZ}l~_UA$&*V`o*&HkDN(ElOW(%r1=g*k|#Kqtb< zOE^IQwW=}~p%gPQ+Pn6vB-`HsK*FO7vHLpxw{)$(mT=Fe>$3=fWsS3cFGkj2S3}(3 z^uO78s9US1MxBf#49CfZE@N9od_@VK081ITFL(8!3N#IB>Sd>OK=d;|3hq$k#4=-J zd&`y+uA|bjmo~4BxjE)s1KHjqj-Ry3ruS$>?L82gap8XZIc+Kxyd^fpRff=Bem1(XrTaxtZsNNuqLk5eSn>@sxyGgFWqwyTuIpwi4Lv(mn3M_Rm6sHi}=?*4jqA}+B)@)x=M zzo4P%r%N5N?b9pGB%RSe^dz$aR2yU{0z~ZXXxOl=Ljll?o?0a2z)0Kp0i9nK~+JuoySMy*2`~ezJ_ry3_K`S z)))stI54%5S+~2M$!N?}Do&M-&QBjl%-K~sW)jeaGY>H0F8(EGmwV_>{xI1?9X7oq zKZkp~z6Y&#v%X12^Mm$&BP$P9iPK)7s9o?)e>(j9+73Jn zrV-m6@x^Y$V5H{o^-`U#g(MRgt9KJc89p%4Hq<#<=GstrC>$DO>tY|^C+XE&E9z=gmh_dN)J^=va+jwVFK~}uX)d+>i|I&>&@)N&;R!}C zVh$%4Z)81#IU02h15UzDl~!xY(!@1(H9l&Psq5-Ga5#A3ftfT8nY)VZ#{n>6A42-*AEF%-*Cy42oiD;e2Zw+l` zj2?A7stK=b1tNRtZTn!1I144fcHs_LoYk_m zPCEeIT$^v{Z)s*6o33u`Gz=skgfVrcu?u*~I0E?Xc=cs#-v_AL5BvT3zSqq5A0+l4 zXG&7SR*{d^kl-j=4r=05Q$Uae>1YpG%rks@aM2*7k14bzx^>^MxnYdCf|)Hms$9zB zD`1PfvA6VDz+PA7Jn|kwI68I6QYd$=TnEzdH?___&=bim5Luw^?b3a9ovGrji#AQ@ zvG`NgGg~ih8NBmX&6!uNk#5VK)twttr5OzOO(@w(?ZA4nZi9J^9wGKA`g7?o0fwL0bRJ{&O^<#r8MMGBqO?1d zz5#mafMlKh38~9W!hKBn%VYc^A5?frk%=1{)=eHV_@I31A#8KpV0V5Jao3{1MoFs| zBs;7w4mvY0Y^_@Ap0foPmEmEcd5bkVKhZgjbCrwblK+zD|F`m}?{R5K$i$seWOB5i zQQEIW6Ec8VC2cT>up2O`fWHu)-URN!YzbiwX|Of9hnVy^-oV?>%`59_3>G`ODE(p! z|MkVd*L(ueynJ}FmleXCPC|W~n*ThTu6nW~7T#8Pc=czC4sJ$tZgn>@$t1k3YU&{*tUBb5 zEclO722}tz?X|*FUw234e}oV&WRks%_?h_rEp8&;)%wWQqpMGqsMm7VHZ5ET+# z|FTI@NYuBuS6a*40}UwHqfE-6h~t_-%fgj`6e6Xr#Eqi7?lJ6?M~Rt{dDYcrH4@k{ zZG<$KKJ85#yxAhtzvxpzBe5^Ldr6U&FMM5MbiSiIvhX>yEeN)0rN}PIjK{dFZr%2a zdn!(xRVn#N(t4c=+jx>t@IKb$54rJI#ZpY*a8wkBz3AZ*?9Rd3QoWc%i^O#)3!>Jh z>KJ&~IXKK6Qqjj^r|AItz2dmeT^fjq*rCIql-r4S&X7e!y$C2gdtYj05(s~uLY|oI z$_o?=;-3r+H^XKTH&T$f(n-Y9O2CCgl8$QmyL96BYHcujZ;l+zX6ZTE<*f$#<5s%=0%?(L_K`ne_|>}*&) z1#EE)dyLciPj@|Tudo;=PK4!6ZEi5~f#cT0hZfWJ-z)gqMJ{uBioxS3 z60J;hxMRz(9TMyytcf0J^}kq-|IYCI^RJqO?rAdLL!EpDYJWCMuwk^_0}LZ0Hmnu! z1lSq>%pS;NtBsFahR^AFbrqQjjCs_hlv2iA#70}(J;FBt2d1N8uX^@|q@9$XiIT-W ze5!2J<=_bHJlHDT4C*aINde#dL*>*`i321$Omh)-Ewi|a#wSM3`{l|_B9Ye>RnEW! z3^%<>hTK7n6?$tjts6pT-eb?w3_F)J_9W`K*nBi;7PyNOI#~2JWdCMy;AlL za-K7u$>A3hS@s~h9wB-u)i>Z7M{sONXPGO(t6>@t+qmuOOE^+e<@PY`MqPRV>;-GB zoSo47{{92OW5mHLprNJYLTmx7J&#cUd-{`I85u*!SQXj!&#YPV>H$%Fm^I;?j5E?O zaWuGhBf8k--^=R1q7ixgh3r0gueE~^o!>F7W|$XO3Rho;-5l;shtwEA#+WcZEg(bP zHR=p~Q0NE-rcM2VNF4C{1qU+elg^+XAb%K%0}xHJTt`(3S&};l9vAYh%=I)xCHuJ# z^O5;CP-=XdjIi8S5h7LMO8Ncs>PHS8&U_d|kw8bj3$=pzkJR86mqXpf;kqU`P*DO< z5Ro{&21XsRf{7$Wxjb1ecVNFp!cLJJZGF5}Ei2wW72vC1taU$!vY zlSIafz;;G&cx~n9#{3DXK?2RDEpMzw(vELHdO(#C*#!_KATSNYWavdW9K3;T$~vY= zSOep{Zgc1n1vswIz)gO<=1QX)sZ0bi03c^wDHTAnx++dq$D$UL#3+Ov#WX<>2*fBb9O%9TkxFf9c|j0PkBZp^iptbfz=3FJ zO$TJx??4%Lu)OpPw_bZ4%;S`is4cA$WPQ?NUKRnn-)Ur{zFpNvBz1njp!W9)r{>@g zp4lgw7i{4LI>Km^;L6BhvF}Ej_VPdbjQ>C$c{@N1b?@!mg~ACs z4`Gmsve6RAAe(iUNtJQFwE1^BTp6z*Cd77q8yF5GBcQ2#5f%hXOdvL!uE+(H?SaCL zhQ{qeXFw4{DUdC601PaWRR^wHt+2o49|mySum#N`s@01gXKxcHGA09G|8^O-bsGaR zrRPcEa4GA^toU38Bq*9V8O|psImJU7A_qE2`wCz!<~3y_pK5qSy*Y?Y8s8E~Zn_gC zo2o0YyoxJG+|AO#ng0no<4VdD)z|e+i?H)x)9FEbBz1~=H}Dz{Ii%bShGhVA`H&|! zMu`NFJ*o0KJ)pa0n5-a(HwEqm9p>0Qx%62H`WX~6AO##V9_Z*$`M{IVa`d@S__DIu zEwV%)Cx0=cu`z*=4ity*@m~Sb6p48ch_vPK&?hD^_%?=7boqW3b!)?lhW!)A znaR!Kj?zcLFhEQHr+`$)9rtIwDWa#(2@J60+9})ZQzMbcP99KEYPl`_=k@4!q*(I=5Xx}y9-Q8q?=Ie!Zxf(0 zsq!Kr0qC=6&_9qQ#ZJd1&K3%j;GUBc+v8)ZdI1N(Zd7OLyGeSC;Q+`n7c!wJ3>BB- zw6pVAgOpe6^F1E4f28Iss~YDn0;odiEU&O#@VPA;8C!877CF%{1~xTENh7uIFBhWV z^gKzi!B(X~kOMe9IJ|x}6xYfE#&XAVB$fd~I5tVleP!Ws4jDJQPfkwE26X0H%!uu| zp<&Eu3Z|C1et3A;mk#j*R@?zXoXShG3Grm%5R@hh!$)|aD@{W2mJXrBSwD_CI}KW z1H2s2TlB1u2iXG1G;px~=J7AvU;nF_@EOHVfPK;EG6EODDL}1Tdxl?RR$>qZwABmKAn#3_@hpj+Sj-lAk95B5A+5~m1?OVTp?wB6D(iiO zrOJZeYm%PnfbDL8d1A{Lb-W3I=Urdd1(<=VH7{621t^ML7VG7YpY+`p*rSwE-x=Bo zAPz&keRvDFOMAD>9@{xFG2s^HRRrXJA|Ku+?I=*Nn*Ghd9V+=WxKR^^0Q14Z@C;dl^D&FH*kc+JQ6RHzhZ z&4nZ>)PUv*aRdalT%eZTcYcMp)UE951VE>?71RcnHaw6HsicHe?)TJqEO!DPrPS~< zS{7a@zDf&b?pywmDAu4+xB{&SYdOZ-h*wEC)63IR1|z2?hp(gUE@ z%ikR_5bt}Qf*%Wo@svo)y8t4#n2|tCHj0xWIUIsuH6|Gs<58dN)&*Rb-K7L#?h|LC z6BaJm^nl3Xch&A+#&tkpaaCP!*Hg!>Crbg8WBvm@JjoA_JMm$a?Dn6*HgEYl4rYv~ z5(l4}9x;3ig4O#2y|TG10B+qJ=kn_Tg|<)k=6y^|hbZ^=HqNn>Tj_6ZI7mw7wiCJD z;R~z4!EuJK8xZMQ-Cj`W*D@MNEpUA+-~(PXfta!a|pfAuf;z3z9(7~r*4cSobv9~|# zB*(<5D8RIa3|;JLy(Q_Y_tMU;o*m>=h02{BL~_5c7&72VKFfA(=1j6T99Rn_YU7V+ z@&F@b3L$VcOkM{LbjEg+xypQ(zkW9yh%{EeB$8l(z}$l$u3Ib?9e2R)WtGq{g1qBx z`7ok>x`RE;cyN%;0|bp<-#h*{q`t6>i%4fZm}>Ylj9WU5i33IidiS)t!Rd@y@q+C15V=FuO+#A z76IQ}qkn)N=>_D$XeonD8fng6#qC?sNzmyKbwrO9+ zl@)@S^*~teM5}qlwEog`0PPka8?S?C!KfEEakwpCC$_nUjM{=h_*~+ZZZ}|3?LuZ0 z&;hL%dbEx(9o7hkni{(taF`KRMs#M1>eddOhwVZu`_M})pCsm8)5tEf>a$t`faq>^ z1Q8;go&?n5kS%*ltGss+6iD}DG2}g{b=MfIzNDkn*Zue??)%FWa^x;o?B?m}p43(Un%bPeMA#?fg5^m`UQF#?N2+)(wa~Eo@{!}j#Y9)%76sAh} zL5CxKIQHDt)by4m)P}Ifua4JEYE+K5QK}q^E`kqMZLQq3^f-6Kc9#6IpOrlSfD^{o zjetYLgz7kx!zt=|xlc2zP7V@lOD4xcep@ltp>kteZ8SXTg$<)iQwy1s*QjKE{s;vK zBx2c1jJ@r7e4gl-Ds8Auhj@o~GD>6Ffddy2RL1=6KtHyLlR>-vC@IQgBb>8U+kz5# zvN?$V(s0B&yk%kRz7aUJg6UG#Lh@dwruz2fUBwv(%HOyLt@~Am2mzL_B8kgShv>G0 zq?AsjBk7}B% z|FBEu>J2b+Ve&jNCS8&P01H6q0fE}vJk$pBa6m1nJ}B#umP%@$r;c2JYdj%*4<*vFfnNc$Ix<>A?C38%6FGYq7?{p>#TZ4+V_g21yP&AKwPSUdZ@04x~&qRfA zrFse7j~!;#HLz(B@LU@!>FD4Ib037%t<`M8y|&u9nkUa(oc4zrPwCiv7N#tH`ygh` zl!MgE#f`=Zf)A}4i|G{L~_D_PIa|LnbSIbtJv4|4o*rB zRDWF*0bwrO0VZ$Vh1$^j_%QkGT&=X8R}A){=jEdUFMS&|e*%))8iGKjtJwd3HUY?p zh78PUtdz7x4@N<_0MB#+@v~u)aXImt^G7V?-PGJs}69u!?;-;U2 zNTy1f&v>Oz_i5GIOq@S^n`Kgpp_kZgdR9Uw#~9URXLp>pagD`DE(B&KQtD-9q#tzH zJ2yTGUe|rh(k-)RSiMum9-4N#Q{X%s6_C7c>s3E$yWPyQH_5+Jw?1Kpcc^p}&#Bz} zp+H4JEwk_J;5vKSDJ=Iak;l>XFqVSv>f|ZGll!EC!LGPMS6qB#ZfDpck{oD#N3A+8 zp9WrLy9)T3zKxi)W6<$jYm$L1EziU%W-oQ>M=;fj!En*6%1t6H4iKnn_MrK_%ea05 z6YZPy9UfymfT|-MG6V-?Wsc?X_EpeP;Yk7+%Ru{;V%VRun>_PKK4Om z?ZbqmG0*9QifxMK4kw=Ck=F;DODvgWNdKeh^_QOPdkt}EZihBpVF<*@vOy%4RC@C( z?bPEWr^Inl>9hR~>=#&4eO^KFIB-CwI{8Drpaj^!{sQ=xsrsE9VQgX?gug``E!W9>zB)3+lQr4w~I@Jr%5=7{x zN=%aw(ECf*`=%v3^CN^FY_}U_o!zi#mN`2Yq8100uov|oD1lvOMY2m-J9W{x_ zisFr(QDOogHM2XC9kFi|#7!QCihYtz_SrfsXNhMQNPU2D?Us-Nj7LCw-21a{{3C=5o!m|^D1R<-AR}BHu!7w{?sZaIBm*S`e|E*wa9P(zs=5|^rW00-K*LNe&)}=)a z0}kkLp>GRLE>E7R58I+zV6CfRQ|hm z=k}`Kt@DZ0Ak=AmqaHB3XlbX3C+s3boS!Ut1e^tqyW)HDaa9+LLcGYc8n@vIO|dSf z^+>9BOh~nFg!tZo@y5UGq`Im#WY78pwU;18q8Q9zs z8{^~V&2?WVan6yhvJsaZsmXy-a>oM^Dsj^@8lGOFBr~{42*^ByTIP0Ya>qt*%g=s7 zJEV+s$rap}sW5S{@a)A*eH>38xh%Kyeq(^5aWML) zlAa-xDB!x{W|$}w7J%ayUATg9ne9%@loiy8VcJ_dQX^oxe4}=V%~;G`3`m9aLIV(5 zuL7$^Gsg?H?b7V_PUH2?!1g7d4_B#xt6_fWTT8xP3Z+JdXx`h=t%m}K;>cxWbR0(1 zG>T*pDZ6d1=f^MaQVkxz0-DOcnOHk)8Y;J51!iv?KjQ`l#>tc|*Xs`7VgXynl(hFA zg+Zk11?*D6^MDZ({JAp$T&JoM<0`wC=-d-{xtwLlUS2F1CbfRNRk9|z{L(tjU)0cd ze0S>fh#gKa&{>tTZ#1gj$4}@%%_BkpG5*|OXuqjB}7qSUEa>qPnT>C|d&eHCO+XnaDw2W|A{(OP}ocm`#g`yF`Y zxy#bhIMvrjoa}P1_e`2xmD~TEJ(b{DpW7!o*0NePkxbgMWubmJ+}6y{ee|Lse@Qa5 zu;DnH|K(zTu3kl3k`kLYc%@-pcCn^NcxWT4ah%S_7ZphNML6UvA>F}G2jD{S?$y?Cy{`EW?Px9$m8CDcS7SyH z5t=*OG2{_wkP{l``$gpZC@9GrDlUbQo=(OZ6%dt1y?decxqGt-sU@Dv{3>kRa*N)g zUZ4N+@sOY-XPSwQ5o;=Vu5h=sK8jkNN1$zb)xnm0%vdk0<_M$SCeL`ASJgn7zanPF z3&U(b=g{BANROFALFm+2a}>IUszFQ_4X9YXQ{%l@Nvhi^08T$i1ZvaJA;`?cO~*t* z3Nkf#a+sX56aXPS@DL(ZEDo7HD2v#b+F&nbE+fd0gHw_aNyhS1A=S$xEBvE;0sd=G z%!gk8O6gd^2l2P%^6-==uJekpbi6&y5#G(ft&=}$_-&ud_rv~&`^Spfwzr2iShSej zF8>m4cWy)p_em z8{Qd5JG{m~Ac;BGYekGs)LFZ4Yp3Q^_W zQl~6;zPe(-0!BX$Pm5NP`|7BWieq;YNg}FM{rS}@O8P%J&eh41@)5X;w5H4`sVm ztQAxTr?|>qMFDK?NBPPTOn4#3wCfuoOFEg;Diw(V0__b(yx0xfuX~Jp6|xy zSe3wxbGEULVF`znL8*2HQjSUel`{WaG)R){<5T~WmW1J974PxFx)WT;!u3KKzo2Cd zKa_V$QogpQJkK~BxH$(uDWGpi*AG{UM?A9w&|(G7@~2C_>9W>eUQolx7<=M)Lf~XS zHVdIa3X9pvUX4?#hdS4sHTGh16Zx@}$s+vyz14RM+`wQ+r_w8n*ev>#wVgjTV-0&i zlQGSfUA$w6-<>FFTyc&$ie#+Y5JTT78s_!L^#gDI_C}hhT2!wJ_FBtxJe|K3b0sh+8#_))*Wuq zT1R@(R2$9%uI^TU^NF>9Q-yhJi&3{Fo4L8$j8S*##M>iA~wwB?VD;#euaq0nY$6DPNYs~ z4RiWzu48gAjP__QzL07WZ@J5xZEuS=kG+b0OEtb6F9VE|@Ul4sp(xuX~4rBwp( zIk1(}I|&_?V~Z}`(yaY_r{9z_&LbKo>p0Aqbwz7oQjTr2-RytiRss1_ z>zOoC2eW~nOe$FPXPG&3LqL~~xgiGu>-CMGgXM!#d00mBP(j8jW(xq1vN0~}_n=zX z6_OtN7r~Wa`7*}n;R98)9OTnJJ+u7DL4}7~J5PC$(kl|p`C!8eoZN#p5HDwijZ)$^ zOEkgPEndcEn(zUKWcn*CtZT_87FaqRl(Yp73%_&iPbo|0HIXZO6P>ENZxd&Yl0wwA zE{)wV$Hui6(rPR_s!tia(NEan3pGbFa&ipL?#^X4*#rnUGyUK?pB!K5EfuL!SlsNR za<-nU4PH;f3TvhP-Vv#cbo3r+{KJM|_|fkw1OgX<35^G#h8SuIdNdUi)$7jk!>foj z#VetHOS}1pef)Y^Ly<>n;ly|Op1T}64Qb*@Wd~)IzW&CnCqcY)b?(kl$cQuk3v-0L zjO=QH4=FCB(9HennM0f&@G|bLYn*ekIahS613VERh2g~@nlo@t^=q{VHnv5cxrQVI z&;_kkwhDNAI4x1x&of>gY8qeNbDLO6O69^+u*tEJg(L8Kp>SJ}L(UB9f1LjZl!(c3Rl?r%Oign*R=kHPlfv0{c+QZQv zdK{M`c_@t8jJ&V!#1d2AUNLe*O+XfJ5#v$EFU7pwYvmj;@zI4QDIfGkV=ZxS{%pLA zhoOQcKIPhF>GmxkSgApIMNgh7%#CrRL77x zZGA>(T*POhd-;`Qd4YXyZNg1}V*BRBJqb4YSrirw-*bYnQoj6_-|1+-v0HoBfc&As zP$sOkeG59Qln}R&bOrZh!xFwcM+~S}u4z>tXKV!8^m@@&&r5R|ojzVV4~oh3-de37 z1pP}FHcVQu^2RgX-qofUtlu&JuW#Snb#UsEC^$0E5m_jN_84@`pS|L+=S{;o!^@^M z9h}5EFQ*u8ukR~7W7Dnx@yp2@;l90fng)THqocFK*R}3foXwVr7Tl;_pY&P;W;KvP zn|l&)nYR7ggmJ$+)f`B;?Fr@DmeF%p+Z9{~?WN!E5k`u~#4>@Iv5X!k3C`p)?_l`o z@Zs7=w3PR&tX9v}zx`w(tCnZGxsva>r`MjfKisWzIK?#@eigi? z%wRlT4irELr5e_}H%C-QNGDXgQo7X9PF|F!0`j196C*wFI?44eVrGUbPBo&1{z_($&|*4@1=95IL;k=jkKf&th5V`;Bm~ zjd%?3o4N!H!pX$?D_dC#@gVXtY7ysOf1NXN#D8t6{wEHhL_}@X9AspZUR1{^{sqth zdzBc3I*$P!o#hsC!yJg{CT=ZK=F}tbvesZD_U0pnv0khOo@c~7!ljNVpw}qNl?XqV z{lN3a{bC7Wg8!17dGtjaz0_tiV?>iBQHaD+aRTEjsHGFzN46WAw;DNHMWs$y~>je?@u7U&=Yzsr%ao+BsNG316zkF;Bp{;1uA!?}$9!v313SHUj^v62dK0Ue2lpNdfBeDVo zM+XVQhWqxaOw!hMcyIv`%P$8UlWRRe@K8E)giW5P^d6=(kJNp^G_pH@^WMEd=2Nqb zap2xm?LVEkJb?=9#ZUs;BZI<1#B@_vM3RDP%k|4}cfT(@x)QhL3b=j5J^_HLNJP6p z{hK9LRIoN*G@PUou*OZ?6cu(Fgd{dr@XR(h^Cb#MKx|~Aq`Pez0q-Nt#uG*b_xkqb z;%SlMg%z?j-fQhgBZZdI42)%+NtEs=|Mvwwy2CryA?&$)VuFof2oHOiE>PdHHkCmr z0$gHIhp)6FMcH5+fmz`uj((eFTfJziG3lp1>Fw1 zLzrc1XRsc|6}W$}#tIR&O21>5J!c|qx*QuzM_oo{s;A6yU6lbEE@#6%p8ZrroNDLu zo~bUlEe#ZO(n^b@H;2h8!XNKdmyvb}Uwr*Nh`(=o%O02GeA5M;>Z>UEg(`$M-EPUL z>I)9dR$F3mAXZd+){RdyjW?YRHkkVHnr-RKx?rt zkHr7s1>jMy72xi(Me2?WIs?_%4K{P5kZX%X^4`SlAP&tHaLJFXSLZnluEiQT@0X%~ z^vZ2?ENkDpMc=&#=atW#(dePkl}&W8j;_I}h0wklmiL?!aa~5GujG}1+yL0p%<)mb z`c0+zSG9({Yj#--E$@w7o?D2e``L#883QGH$rA)MDW7jS1<_(NfkU%q?eXEYhnL=a z*B_}4p3K~obN8V%2bp&sM5Ju??QCD=ZP(Nii{X8>&msLpaJPllo34YVrTwes8!@pC zvUdc&mnt9ow>hT;5BdQ(F}BGQr2Hv0+DV#gDUZalPy*bhmsRXJe*w|%@+HhvvEQzW z<`3$>T>Mgd=<|LLw+?qh4BLo6Gb4UFsPaI*An7WqssZmvAwZ1}c@Eh%<#elI*G!1? zIAg=U{(0-NzNrxX9SbkjR88+e!8#iAL`vg*tIM&L3uXPBTcoBDv7~0C=a8%taSp*5 zH{y<<(ae;b=ZXvt{L8_eLDPx~PRCM0N^=1Qowk#IidDJ0Hy6&{h7a=4OTpQ4I(=7+ z^L& z{qKGqPyt2U{%SZ0PuPWrVomx6p9{<4{R)-q<2u>D_9-2oiDN?NYNlKWQZZh$J$ zQAnPw6xMT1gms_4x^~w*C_r_(40*9$Vb~5{hB7tmu12FA3`ravoa^j!Mo14d2b(zM z6;#L^6R{%{b2#o}xsl6qFP9{V@=NU1lAP9pT3$qrzAH)iZW6ptzXW*JsrO?`Xf9V# z5u`j&Vf?tmyrncdW`!CCQD)gvKo6$Vhd6Fir(l()Zc}-WxY|v6ofaCc0{3bS0h-6Z zOa=nirA%b0jI2spS{3FN(qQobFI&xf@A#cxy?Z-=?oep&0Q5 z0`(q81Z^SYr!srF36G*9jGW(G>IQiWdC0PmY3MAVxE`~i2Xbp-kLUN^%8dEKeD z)KR?|C3*;go5Ws+#?TMTf*juM+S%p`7Nb9;Zo})c?o1dz@aP-#ym!BQUi+36%KOu8 zD_EqEe1gDKN9s{$dXqLW7sB=fR;+@FBX+$$#CbI4q|lu7e+XGA?D>%$6})|;9A6>F z#mbL)iihqvJU9{>VM;fxu5RF~-eFW+RDDD0{X>4w-#Fv>KdJ6GiKz{S^3VnMdMo^t zT(8twKkCq*XCSk-HzY)vg#KIm_J^}u*4id)#x(Da_M`H)hfO*zE9s6}q|}($5y@D! z#%w6&&=WQdRa({Git0Fb%^0=Wc-}32<)_z%5L;fJyFhT{eiRwdFD*CvDC~C1y~NN9 z8n+T+hxR0n!0?hQ23QuXr#Z`7+eCMEf-|D$vx67G!|&ZoG|TRY;(C)p+4#%~xM0N6 z?)CTM{Fp+RNshP6OnB>o?^~sXa`pluBZU+q5^zXO&}aUn+G%lwGKX*xG}*fjwo(^X zYsua=tJUQ=Kltn#bXw_ZTHZR1XV8CT+HhBTgs@C`U7CTtUw1uoEOpc@Zc*;4Pczl} zxl)vYBv+e8(Y+nt4ejR|01cX{;M2g7cgEJUgB8-iNB4T~67tegwJ8CLJz&PzbO^#b zRJ(R&7&erLNg3cnvOU>)UKqLigN_#JpvXB^i-Br7u3euitY$S%yN?DP95Eg4%G|iU z@+CUTIv2|$z}H%Cm|c-wM_%l_ydN-N{s~U}rtY`g7hO#fs(NC8FzA$2?W5t!@Uwd) zJ(l1+pKv7i#EEHZyVu3(kqQ#!Bjs>eyZbAz^D>&wNO~a()P~)bnBD1WojtP`CV9Ei z(^FhmcNepK=d3r*zSE6K!(GVNO`h<0FrhPborRcO5ls!hwpY32t%kt}NKf{8jdpvy z#jAxMere)X!uO9olDhuq7mmWZ{h*CX z*EsldGbwaI4eRhZ&h*5K&S!RAXjl=4eJBn(+37`qBO~hi3eWWzfax4Z69U45_zlnS z8QOl-_npajdoR7@r?IN@Y7T1gnFsP|kBsc|BV1Q&-AA~jYwpTAUtWk>)zcgLRq)%+`b9H&u!cl|JI&V=`M$f`ewVsvgQfwFVrhEr zpAX48;4O+Ph$}|oL&<%$>|P$&Xf^bDA|K4qpu@x}U*Ec;GApJRCj4E%^7|V3@)#gq z$u!-J))e{Cv(B@g*V-;2);PV`_Jh#wmHar%Pt)DJ&Pc61+%MxZqhmsytyi99TvRnsjRaOBe4x&>{NYiy&;C&@V{K5hZo z_{1#Y;153b+pWhQ>%Tr=w;iQDI)9MBIG=aADmO+&(+TBUYA2_azn-uC9|y;Oz5JGm z0ba#lupH5spJ>9(qR0rE`75W)&Bh3R{gZp`%X+@3a5w$frIWN+kYm~JE%w89=#+j}vygIoApwaZ7fF#DKj_PIhZ`^( z*>6;yTLw_^(l8pn@DstZ@y^@6QnXzyl`p(L4yv?}5-;Q^o%pI{tGYt}wS&9DzL0_k$Pe|JefHS$h9$f$uE6f40E) zV(CApz;|+ie@=n#9GCx`0^d0<{{ceZ0U&q(0Yd-xgZ>{N^nV|p-~RswLU+`D-nMg3 zxZhGWB-YsV`Jg#{!6NvG_51!lENy6RW!O#G;t6+u)W(33{F1}^boY7dHsEM{^+|Ad z1uWe^>E~}lroDDrCWQQDsP&df5wDmIc&((ukoSpH0>vNq%A zS;|k~y}oh_D!AO8M=b?*7b4i4odIQiy3HzIvN#U@7thdm+Rq5aicP7pXV9(G&!OT4 z`yBEsRRvu?2cFkjn~HWc>TblcXyUQ6n~MjlmsXY-hDK7g?w1Lyj}JPY&M*@!HWc_L7P*qbr!ZA1F(lQW;|+EG51=jNH7e&kuV()8(e z&^g@ud+B2l@}r^aL`;Qa5z7~*hsKBIj{ZGo_fTy2Yo5e1yW0Ce9b|~ZF@QRDg(d)< z(l*U(IV9(Yu_4z#ShA=bL36C-Hu3o)*%Lpg1Ik1IcR=^!BspkN}m!Xf3oChXc0%Z z4`>sq{HEc`o5E!};ziu<2gx45bBF{ScI!B=CQ-Y7pY5IZWk4Fs zIIxWa&I^{BLutHCs=j0K4U`-53#_Mn-6fMvLN?tAax-$xK4n>MV8hHZar|4V>q`DE zFk~?ZOj2sKsoTOvQ1@r4)+qLugHhL$3!_c!fDc~TmeWa#rKo5`NI9=U9lvRh6FHj_Mm$u%d*sT}v#ygoQP9zIjq z>%(t7xw(K26)2c{71N4+ut9aM3I>ge6qKTF@SDgAd^~o9TR?7cYia~h9g6DIdaF4d zg6^M*3D;=|Zx|ou^${OZ{(#uVjkx32f4Wu3QNkNz^RFMS2Hk1%Dz4*f z)uR}^AFL_uG_X!M)rxx8d2B{z02%kU1JBbLpl;y$-zAz_~Hh}(Ky zy8`J=)=5qWM3cR5CqSDs5ZgN-OT&(b*YnJqTbF`(midicV8a1w=vPCq1|X9?RZuln zBJo}~z>@ZHZg_HNx;x#bhCWKSk}Di_E~}Ur<63o-7>R#uC8VV!BkZ%j(la|x{20hF zFVZ9kzs|awUiy*AbNMhS1#xeW5Y5uggYQ}Od{5Zh5&PWOZ1gvg^Ut09ufQQbN>-YI zVgSuznI_EKHqSsw_P#@AoP_6jY;v5QEA?VBNUu%mSXBSQnis~@ddplt( z8F6Y${Pa(0g_sunyG}tPM~s2{th=X*ZI=y`)}$%`O+}wI8*~k8AD>DpXwB*Uk~j$= zzb5HDd%8yXvn4Fs ztl0iBd7bCu37!#=R>=V|LTpdsSqa^H3i*R)7QE&pR`e*Mh!Ke%8$C@6D#mPTvK7DvFdhAws-S%B<_2 z!g!}@4P(D{9fQhssxxi!=1QCxuCY#iCBgXy*5wwV%`9hfwIQ>cIxB9`BvjrCA|-?_ z`~q&E5JMhvh^km-xS_uGyNZV+b<#_&q7LoKZCM+U#!xPl+ha`<%qHHgos~**u7U+b zPCl{zJu=)dS^Dd26XUySUd(G$_zLP@xReC5L06CUv=+3@h9X5)ce2fH2Ie9bs+zWM zI}lUuP`TdIz0hG~`EdvTvnUAbq}+)ATu-}IOAPS15o?P_FKjBs?2eC_d0$l08;NfM zaOA%A$?~2wobO!z!CF7;g8;OnGF;e?)~vP``11Z|$7&(Z)UPszGaYC7*FKH&s8x+t zVF0iCMoGUr8NH%FB>m|k0FQ+qYbn3 z0v7Crl{;;{H9opt+Q(}=bdP`1F^+^RPCFhdArsi==p66jKMz9*>`#|$koO$<^i2CM zNmZ_g`YWHh*jxWwA%>`Qsc;PC6d!S_7Q%vVL=jbu>oUX}G{j?)dLmb5^niLcwz zty*Kd(M#2^1rPZNW{TcTG?1G6Sr1aeAm=$PfkYmya-~fnX-vrD%t-! zjJxM%XM~NdAeVIMUB{AURfCOBD&%)6qMY zqf}5`UNG=z8CA0OxJV^O5eSQ7IqXN+)?kgJ3!z622taGhbpu#I{FdOBadXRzHz~!> zGw+LU!?a%f>GikjQmv5j`RY#W_^?}tM6Z^KF>n~Q^ji4HArhG$c`4wnO>O-qzHu)e z%74}*VJb_~qy_S9kOc8l%W~^%NxDoMJO~>kjtd*UYsoFBkX3XaJdaO(Y>nKQi;F=M zobmcLDP<+DzLmtrr1508&d&bUkP1Vzi6l?76RN!*20UQam|OK)earTZ)eTW$zxT^s zX-=Elu>C^<6B|*N{SgT>Qq>;SN}7sR!#sYs{Zs%l?cc;Lb~XgiMUrEBc^F2}a5jgy z^1BK~Xs+d#_xEs{=~0T8r;^<|7>7Ck+N6J>-F@#h0$aAX18C7Vn6Vte9&;rqkF44h zPa>C6eOsQx2H%XQhVbFD$j_N|8Cu9V%Yl(5xSc$6^@K)*fq(Gu9F^aqu$J6jb*uLQ zX06`=Cr@wXC>8@St(W*@YdvB{lTYp!0Z*)P+1*{wWP6{?AJ2yIRKi79&6-Od8(2fQU-Dy&YAhgLUg z2fb|S`PZF>3AnGfh*&23-V2ZC^ed^x``PmY<(5FBl2FlYPy8MQT|r_Cc(Ix zjj6VH6U5ghAIJLeP9*fuCoz?qXXM0$glqkMQG=NbKdO%_mPu>PyOn6|*5^6C%`SuI zcqn{*p6c&^>EV^~Dqr)C(HPEadsH^oQLwL9L##b!1RT6Z?|F)~owscKR2L?tGyjVU zbAsvEF^V4b%{nJYq-0;$QeedoIt`etiZJ{+cKijVznS?v=7GQod7Ue`k4r}9-DDOL z5sD@(3ZUI-6>dc&peAgA-=g}> ztj+x~>zcWLLdh#>zXq;u+m(@g>nKH3lHb@H=3Y5P%QFu6`ld%Z^tDLQnS~dsr!aE` zeq)8idK43lcTTcB-+9@fQt>LPr&GgksaDO{Wwg=&*$}syehIR+K5n3S%1Ii;flddz zOl*(U+$NsX`&;ZLyuECoiOO%Bd{N^~C0EDN>!8K^l`2x;pkFV@6seMpm^i4V!dr;h zR1I>g%{*t&Dcot@`!KUrf*E+bS8ud`BXf5FYqn#c@dm$HVqaXqT`k^vF38sz0S{;m z)p~u2eS@*uD!sDSiG8%YttT?{DvHfQ>oNS0Oq9k9$23Yy<&2YDw=3CY7V zJ;W=ozPB8I6C%18>vtR%2iC8uz|*wUIlXHK$nAN_!D~v>Y4xJJVypyOYY#oK zo)K`FNpi8fb!~s<@!jI6`D((|Zf2!$j9p^cE%vbtM10RG*!nj0;+Qo}f@3X1owH`d zgS1dQnd%Bf^JJoO7J#v6+faXX6LIsBe41=z| zT}+L+9gxXgy?#Ucer5{jY~g0?I>U=&Iq<)V0YE`gJgy@@sm^c<_rEKh1xM9>Hl6TQ zfi%40*2V{<=Kn*+II^3MbW6ZbNWx_#CG9>p$TmT#R-cylpC7*?W8K#(jW(7j`oOSC zTnEs8dX$O9O*`26cC5B?EY%9Rbt>yo?dQG;gMV>??|HWZqKW6sK8L(fDM*eJS75fj}-U7?kqp&w$!3+g|Gee(MnYYssI-a*= z+D6<4gbdbt?&?zQF0!++v)kKmXQA&HMArft?&JEHV3HJeevG0=L>U<7fD@J;bwuX0 zDPp|cn;688A_M0rB^^@h#GbC#OS0U2Kt}j6=|Zu#BI8;5I5^kZ6>%irFOj+OyINn&<_#YLpI9t-G-X}x ztq*D7H&mn`Bm{oELm8gWd9a=2>5(aD9LxQB~X^ic}%sVb@Y3>@SH zQSEX@<+Thp6Fbd@waND0p<=Sn6?ZDzAbZa`*JL~Nd!kLeo~uhAkzcTZkq9B&VOB=@ z4~!#(N06bDu?{i$dZ0&z7mXeC@d-!Mq@J|aHN525su~aA53U!aO$D^=(NNi=gp6B)P_y68=2-*xuA5dniHWf2Ryb~L(gC^sm zI%yl?jFRbChJl-6^B;gC0q^uz)^tVcobURyzE9u9RrV94f6s2%C*t^HOP};#&A+_w zE@p%}t+Jc=WyGFfXktBWJ`I!a4n~EYh*HR_ILgDflS^?nejK&2j$o4saB=Ho*l?bw z2!e6Tv5MO7gAWm|LATj<+0}n~Y`x`vEeM-9PsQ-4xM<~7HqKIM_tZrAc~sVKVpMFe zU~PvhK>h^6%d8B1R9&4D9iurw1^VUU?#3h=d{0=Kf#yrzqq4Ry^j^oVwfy?#<7ccI ze`7%+<<-wu8!}3b^VIJ(o)0>0$#tzGMruad$FS$FyB!wX`s5!yp&BKuLyg)Cg` zbW7|3SBt8wXROphe%Si2Z(_z~0h>ZLHl9Y_PO!GE>U525SKR5%#{(vRBaN>D} zkBvORG5YZ3iI6iI<(V`auB#}3_@U}>{x~=bkjj{mkzdP_^e+BJtq#J2+J2JtZ zuWIa>c@#R}-yKA1Nc9V6A8^Ylyxk?Spcg}`w!^NP`D%J(sQWrInPhfr4hY8CZ^fF{ z?^Hg}`>W~C2k$`H9TSmCO!30$J`{cXw&T3=nLkDJd{DJJYYn;YO!|5)>X|6gzY6rF zcvxoSR7Uw)&(r!v#P{>nZXm8+^~5q()sNSdyJAuw+v1^Ur#t}Ven%i12e|+mJ%?W^ zQeU4VKpf&WM_gMaEuJ^eBrjbP0mBtcp!#{+vf%mm*>{&T|ueAa>Fxs(n zLz{K*$&rPwvxELE^ggFztjpp^FNJfa)&knnM+Y3_I;P+(hE3|EtSwA(o!u3udf1^i z(*rXD&WcDlS?LV@YNvQBZ_h=LYFHLeyi$K;1R@|W$mk?RrfQAX{LA9g+Qq-Ao_sS! zcUyn%z2B9`;D6eip|{>n%K;I1)gS#C8L;02H>o#?JuKFrW%jdV&GKVdTd}CM&K93^ z2{6t7!6&tFCikqYSByu(h{dO3^)J(E6ar$aOC(R2&X0(C62=KW8(f_bes$@x2jPd$ z|FTIV2du2N7Ea(>`0tNSY~r1pIqr%%6D&K~pSYwqr3$Qe7X>ww_z`a)*#7C?H%GH9fn%BUJt9 z@%cQIW*Dgaq4i^Gvr1?BQVN^z$2W9;>ItsrQBv>(wZcK~Xvx)0Fcfx#^{;PjI!UeD zn|gS95*-R&GMA+Q!lAMn>HxAj0y3pr2@^IjqeMD+`UDU1>(Sn#aQR_x?O|WM&_PD} z%isXW!WV6eKAk2}voNO5-`5_TI2juQX7?1`KFI_f5!MxJTfd6~CK*Z%8uDK4iCLuT zEub1y9UH0ZInF)M*l!ep@H84W2Ws-`_?RXwk!Vc3ztKQaXwn*)fM6?w5D(^?j#=~F zs)soGCAO^2yLGg-0E;Xq_ZfdHjF}i<1)S|T>?TTpuw8U2-ZE0ZyzjzR)vh}qKLYDH zn`PB5c9c8q-@DV~o84KFu-!;IaP#|c- zGPYfMuu&?%waj>oXR50hhlu+Xxd@wd)ri~j4Z#9VsR-g^xe+u}LmpSrwizacDSeSt zDVM9Bel_dfn=p0StB%SPsb`|8{$gG?@jWZ|SyR9^^s}{~M0sKB7MDy}%z)a$FFkzd zKgwRS{ZesHqPp!u@#kN-)arw|#cOf^1|56yZ2bunZeC`;=7lVBY(5S@`cQkp5p}zV@1FC7oUzJi^HlPhUh17hSC%^u%v= zzq=KELDUsk6E}eF1h4us&N}y;vEh}BQMRVS0qHMo@mKD1rG581Hk9Y6?E4qgciDQTCr73feywd_7&2n+tK2A9P3vzd2HLPcF$E zw%M>Mb4cz?TgzZkm((wQXHN_s^H^r9+g0kvjS7S_{MHA1LRTk?Et{ZHl2K53xyD0@ z;gWm8^s2%^q0~sDelmmE7=FaWJL<5q?*n3Id^`ffm(iZpT9Sry_raUU{+aWA`@`(h zE57Ay&{d*g#wJrN=)pF{r8ZRsSIhEvaFSC~S z-}JV_P0Usn~m^3`M3S;Vx)Jx|K;ER&yA941P+Lq&Hieaza`WE_kcXm z1Yycm)9?pxDLky=1^2j|p|xCp3(x*{xV_3hr2GVPT9?TO|37x+eKgYmFPYG#?Xo63(w(cbsqVxp|C zrI=!VfEV)2Bi;SsRn0(7fr_m*yNsjb{!;k;@3`>CZywUNL~$p`${KN06Qt7YXPjbt z)v=-vRenjTD5>EyKLVfPcY+v`8JKi8LHk^yfz|8M1i_!u6Ez}Z%fI^;SK#}n2MW#B z7e|zK0_8%pdZK0IB@4ighQ&%6X_kzvm)z^hiyWRIE3(BKBZYO;I{NMUcLeT?II1>x zVlkI1YgX_tHNoF6U+|_EPj_Xg$5BAyD&9w^S?ds>FJTcGL;Yk`kZ4Wp#rjQZdQ99& zu{Jqi>$ZN)W5zv2{O{(o?ASSf$}`HAmTZ|F{E6pUh-zTkx+{_a=cKmnAW0hd8mx^&&oq;@2yxKqWivBk+qt2Bc9o6mgF z3eKHGZ}+96D($w=f)WFTL@TIFdUO zXYG8zi4V%hee{kJNv_i77hAbXi=n@$gynF8n$|?sS#xJNyL1!0P00Kyak<+jP%VKqoVxo6d z9>qSkvOAk!IA;#}nYA!Y&JX~3T=QAB$`lw${PYA7b>6ZUUG>XUlG{|gRpt@aN=s9? zu)wweiZT@abr|*Oj>gf+xiTHfDgxV}$;sJrKdh9o;uNJ%sQn%2v4&P0vYdSr1G>buB}7H7B4jlcO$O^NQ`Rzk28PH9-kpK0*zJzbk?`EthjD9e|5ty10;$0!@EgW z{}{u&dOKKWA8j@C0fm}tmgP9>B^WB|7*0i=jwi8AV{r8DHkqm3H+;yTt?n zVG~#pz*LDcuV+1L;4?bOXXaQO?=|Z4mZ1X3H7q56-!uBg0AXzTL}Vv@u7}zK*fW^f z=~Rzc1E8TJb)sW{3QAZ=Rf7C0*-Rg|?`oSdY#CTU~Ai(uuk zrDP@!8$OzJ7lzX@dV`yAiSp@RiUC!m25cu<=93~53Y@;C6^?-td?b*|{bE>Vcp6mr z%P%bC{V}7v_Ql`-IfgsoqC>v~ATL7zDJ7k*S7?fxajKk*EvAfxX=Q7BB>ztmB}(cl zJ0h!2f~{cnqBQUl3foqA-g4~A3hz~vV!*Z(mH!>E1Dp3LtmUnZ zC)oHs@{=g}Tn8g4qgB&Yqgg+@7Smg$mITQokVW+>NzUVWq9RxbejQY<8g~jfnK<%E zlV|qJv~?+%NlS*GZ97@qXedC^QTDr>3WzSfL!9*?QP0E+<`00P#v?lqQ&X3ZJI01l zN{yW25^)1UKI_k|RE=BZUmqGOH2~#2R?e%$XYMNGQS#m4fTocx!N+Tyt})KBH)5>l z8qYH-30bHggM~2H-C+Hf_#e$>k2H%42>_X~VGXmR+9%0^`1!=%$Pq96rK56=hk>fj z+}M33Cg`_O{)Toc-O!;x7}yJLZtsns;{JwO|MK|5L~vW8tlg`W8!)VF9nREmL5IsG zv(-)m7+4rqpE!64IkY!1ENG9tM#dZ?l>37Uw8)!z!~wg3weU+=OLO=*x#!TbFIm zliGq*Yy`1y-aVMNoV09Gmbi5yaij9#Fq#@l(seX-G)kZqa_v3A5g-MgjkT5)#@jSkESBN`L#mM^7m-_7rJ!pFqh8o5kpMB^)o{V{7icx>w-e+t#MJ~n z?*c7gkiypwCvNA8o&lQfzKZo%n&-#Ec_FQ-4>@@M3TZjV@9qi9klH$d2W`_MR!3DM z1&=h$TnIanzm>Qc$4S=JqM+U85*X3uzbGGF&?NZcX#&`h8W;>f)n{b)0I zv4Yz`j3}^3lw&L@EGMe0G|P`;oHmgWhK={|c`p`_rlw2sC%HcJqO+<_O(iM!J70v( zqgO7RZ;}{)Vmz13@MhfG^;xIX5vxixW|g^?&rAs;8!u`hl4A-Xk5cq&t-|vB7i2_i z22ypMm0X$LsB;Zd%C*>M$lwSzheOjv{-|^=lDv`mQ2T0YD{@bynyJWPWr6@ic_uWh zLnv!;_;gZ9=^O&m>RCBH&HYQ?!2ieIdxpcgw*SH_MVcUuBzhuse+0pY^PFt@qpB`}s0Qju^N5I_vLj z*ZJ+PrRMeEgI_J~@AiiA!7AxxTH*UHquWyZ?9ZQM5LkJpykcFY{ z_MG9cp%1aj;aH$kz2T_2ccei9&+FVAdt*@7IoEx0W3#t`(%S=IE7YMRP`|adfj94f)ZpT~E13_mDULU^Hg_qc4uy9x$0c6gK-2P5 zY(CHXme5Y+{Y2<(rGZeYcwiw2ukx~+vAsaYd8De_3cLs3GXW#(n3GS`WegK@Uyg-Bq(bdQu8k#V~R^VnI&Kc zgFWkF1vj{ECNQ15Oq8JaGJ8}j_Px~xElE?!`F6A=Tv!v>-Afk7y#kvGCjI6YuqcRS zQ>06LtVgMonh1>-hb~*YFK~qlyLcFVE1$lRdM4eqXTSP z6+WA1^AY-4fQddFUll}&r|h;LOei%wZ79T0Z{RWkszFX$+5t0h*{P}#;GC})%t7ue zq0;M`MsH!$s-o&K{LG@nF&{r<6?^tT+8Mx=Rp8Zl>zPfBz9}3Jk zaazL{K9RQaa>aAwt^s2S`hcHa?`;Y7cL6Kg+@-Puet{pyZw$7-ZmP%* zB(@^QUrJiKw5|%Zly!cPN}pENFRFfstJ+MPwo~#w=I`@d@e_SHD(SCWG0rA++bw2~@)Tatcx&#aKnIDyZCLU(Nv*la zj8VZ=s&;6)VvgItuvJ3(YVTxVE7U?VwUD98J^xkKKBv8okw<5F4AAZT+0vT``Btc; z!}H4gTj7m^u_-_LQOQE8BtD#^?V(pa&`ikRkCO4M47q&_d0T1|^i=_9IBge$zgI)l z-R`L)C=K5W6?B>B=8$cAMiz|tW)@!0fH61}*OhY55nL~Jj%%yTj_loOfVxUXjX7F? zV*|#5$;RmUa37^Fb=5gd!JC&3>v)0bk^}|c5@>*AutE~mR(oZAho%Zo3O>x5J1$mj zZS`x=1hS)(hgWOdVZ_7iG7fj-uD8*)H#JRyVf5<|F9P$;R2rbyOBrqeXuwn*)>HB}8TAKx+1(c=rw7#9I6n@x@W(eV3h$0P z_PlcHld)cz7z_vlVyCbA>?tPTbI9&7@Z1Ql7CW8HaQu!x*8EZgWjxw*JA@;B?}4uO zbUBhK^gshhri}O)^4z6ZIXNt&!681?w{~7SK~TjHn@K z8Yj5oJx99!B_q8|(9W>_iDbXF=1VvC4P^237NV78zDV+Q;Vakb{ov$JEPm##b`7d4 zr9@Tt^xWPVj2%-bd3@X%L0v&|j6-#Sp6Hws#vt|^n7E3xO_G98?X?)g4AFCPq7r^c_{@HAfhT_(P`@{+~q--gvR(3AjjTP!iO1%P z$|tinw)OibO$j2?YfTizk;#+)N;JWUstp4J04kBixtwrk)H}9nGk|EnZ^B?o9qu~L zNo6Z({ekUlP4KDm3SHz)eR4gHH1-c|0>B}u>sB(0eZnY6A%TY*T9;{vu7`)MZE|YX zP%~BFDrD|aNps8b2i95#ssR5ARpu(WF?XnAKbXcXJtIo6XiBa3sdvTkQ=08(3&&dI zNo74KG^Z{stQvd@bWA@Pe(w^HE2ZbN@+i(D+)KAX*Q_`c~^=VEd$cVXZVXslaW1LV@A2@WS`Z&At|S zl{+NG!@918BgP?0nczDGLnTvhd!m4$T$+>d?XqRaMv#C|ivy zJ`p_TU70$wkV%g004xhq5OZHN)zd=;5tN}Fz{&nwgbgie+`7ffaV_1|pxN*en=Oww zex%f7(JElh7!nK8H9GHH1@sezjn@KE&Jn-OQyZ@XwZxYCWX|idHg#ULeRguRP8bDk z%p9TUDos&U<5PcgNI3O%E%9~f@c}}}^I&CmOU=GElU&$y5~MOZ8=UpBTRy%Ypx!d8 zrgG+|>=J$Z-1ixyg#jSG&hz8HqMMZmz)R1j9S&?9G^1srZ&RN%UX#*$3E?37odrNH zN&VUHT`Irle0)1Dc*B<1F*=WZBC86esJ5Si1bBJu>nzVNcWkRUz8&92A&+9oDlslmz z5}+Y-pc>wbFU=jhY%^GDfQ9!!BQ=Qb_OS-r?A$IZEFex zF`k>G0bj=M2FxUN^*)$S^qzlZMO>eJmE124o2jX%s*}7WM0+UFYImcPTbQU5jMWuw z{+yxxvibMHpg%;3v6QzYy`C0SexY*T1V%l@U5-%7`yLvk(SQ(2csLbQ@38ED^w@C| z2PS*rUUlUW2LRF*`h-W|0l997RngZTgDxXc-%dh%Ee#gS9iZ`~MVW4=Yu9~aoxntb zVyQ@ZGYvY48euG+_zGQGpAlbe?Oq#IEBJ3kJw=Yw1DGE6sGADG@{T^$kS_SmZ{NP)&G4k$p)fDW|& zMTQVHJ9Duc2J$&&1pqlGIb1$$(l5|Z)blcmeXDisrrk|1dEPbQbQlwjFdXKP+@FuP zA$)r#&yi-hzsm+}*{Q%<*l$NOnB|mi=Y@Q$MilGpox|+GJn9~ljej|oD$~oO^xd4MlkKs*aot(g!oX3;woF1@JpXtEN8ETOwRb09WPYIu*w-g!Y9FOn59ddh9|HWp?eKWXBmFh@9?mIkro& zpLgHqwn%rjvZ-!{?jL-+1iDuSgfUX*F5gNJ&~_d=Hogs#f$vD{3Uo?{iLIo;uq$N_-TN54tc%NZ zS|Wbs)P%*co|dPC1jFuQSL<5VVN1tF&Ve$H``C8{|6vww4(6e#L{*>M0 zsbO3;fRpaVU%3#sn72Ez$VEn-g0o?^bh-Tg=GiNcemZsX;Tx^`da*y7*U2)2*&CT8 zkPT#WaL)9?Zyx#VO`RVt2SwjtQ++DxvU2}`LVjiK+~Ah5gd|ruu$a2z;G&56|zf)qK9_D9g9&U6qk&yE5+T_u<$g6=o0F65R5eyzzd)7t?S0l2#qEN6mwZ>{RQj zW_iI96^}>68qWC{*Lc6X*dHrFl{K*IkwRab`Le?JlXN*dv;qnS-Nt7mreTAgcg~LT zK$ne2UKTo?+D*EU6u@lEeEI_8&p-X!$33QuRleeCoTiXzteC5<`*EGn)BI#+0ZGE4 zwfhXx6{BAxNi1=yz4wZ;V1RD7qf6T{WtCzaQmN;uRAQCr8KLdAHdVyyb7+WTx9bJg zNe8tRm^cm=8>(>El++>8U5y4AOlH@MdW7@04vuNJLlEw*VW|!-h>-E!bTS-|M!u%Y zBs!p;!ROx50Y$l;S#M*#YUaWEIqDL`F0s{gX|VCwB_BIr-5tdo;>an@7au|L2Nu4_ zFl_MoIFE%C-65pQu%R%wT(Tl2{|1|-phvfkd&cooS1G(-U=7Qv%s~NjpyzB8iNyyH4QnXmN z(l)xaMtm-Cj5^`bm?yp-hjScsTD2VKf0aG8(a9YL>=06LnD3gxWs>G=6~_W??(DgXi>5drzv5|FGu16-8;Q29>%#(B_!sa@Hm zpL3@|R&lw5aG>NgOz<}ss$b9PGN^AJh3MeJY?l>WPs1`c^>;chSPaD?ITV2L6L+DI zO0#T(O%)Zmqpqt}NS9t9Y z8b*>|sjz-pBwHug>ZO3ApzI~@y7To)L-w?9|6OxWv>BxJ z9kYAT-kR=3?Qq7dBPOmTv?n5pIk*#w)HVHb7+{;xiTmtZAmq{Dr5F1E;-<7Kg+7bw zNctWJPfJwndlZHun?sfoEtlXYEEB3A;g}B^LV9^jtuDcwXMNteshs>s) z>6M=HnSs6#MjHxXCIw+SQQLT|Y>le0#XPneDxmMbyP03y5T8~zQHBsMrv8+#i@o7^ zoIF?DaQaxR3iDAlJDAt)d(fRzVt~L0{Vwpn(Jvd%A-BeAjrhot7Ul4(ys61shy5@M zmC*)BgN4$9G5?p+Rdl67x}F8djGO!^=BMO zQaQ7Nl-0_CA1Q~*b#&vp60@8>jz=}Ut^9PGKu#N%Ie+Ys1WAo&Q{$J-@=A|Qc4_}A zAeM55<3=ZkhElRJ{=xAD6$` zmpf>yKFKes8<_!thRfIO5lV@?`nc-dshMF^zLwTOJwvTwq0dfxW6#qkuI-^?71kZr z#r5CMPSg^v@o!p{eE3Zo-jrQ*^EdYZPn0Ac>tK7*d4S$>2hB>*K+7nqK}83`5FQu5 zZu2EkQ{u!;vBt*%WP7`nRo%6pU)e4oai+n@y17zFOdwixj#L52jwf5d$r5Yz>Zu#P zRZo};D71;W3WlSB#d{$%%-8SyiT_}Xm_JL!sdo=3nx`Hfc9y(D6%{_PYglFJ6O2iW z+DBCZne(TCd2^=&eQ0U;&gGtSLRr=`;y4G&KE0I--?6wtq#?eaAPUUDh4KYnbw|;M$=Js~$ zEkuX+(5AlQ_MF4I3|1kG_T#FMbB|WPPd=1e)(z;QB>(U5TT-4xJP4 z3PaR-?aNnB=RLAkE~MYxR57eZzxo4{8M99lGZN_;xNYLDJ~sEj1MSPniix~3-6B|8 z(BS9DBKh?Mard%AA_3oDV)O)rD$)-QSD`q61+ZBvkwA#<{vx-wiy)c8Xo4*y714QO zf!JkmKrMQbC8lcTzC&;illYmJUUCrs%|iah))Un>e8lmNOS?vKxERfdR^+%v7DLIV z`ifosKI>4vhLYgx`J2LTzW-eCs#Jcf!f8=ewdD1wWA2xAD8VFA6_(9#ht4=QN4@)8 z)z8Uk#qWE%;O&c~>?Xvcm=$J~Z;sWd`GXNIxTpecn5Z-yz*5=c;{$ogRRS+XEF<-4{6A$xljt+YKa>3tyQdzvK6 z5!;HiKrZs=LY=BM@$sxjw7;pefbrXMYY^{`aR}dBkY$ z3tTPH28h{HT@cjNGQX?Fd0D#+6oStN1hd_>fM+7KtR&PsC3PeWZ%{YEr@s$2gq5I-CKoB`ZJbkC;& zLJ-v^D%j~*rhV(V&!v|X*u}257BPGO1uvH#H|pIVRrKE<^V?6~T)S|EU+g0NV|gBF zw56@?1A`q~1MU-EEl#bZpCaKkYiF+xR*&!#NE+bgJ0l6P{Zlvdn^yeB&378WrFsQ^ zwBf(~slO!54=K-bJmqJ;czDXu^Do}`e?I0>0YFk?%2~I+$LJp~$e2nB7(XmU0{ZW0 z`mbj)rew6F%4cl-gYEgBFY|AQUDN}}APQ+5r~jJ_K6%2G{GX&@uSi7b|DCM=Q*M8K zw<~g3fDHZnXXbZ)jT`^QFZ;eC0T#@$x2^vZdW+sRmwkNL*_2-uM< z9VYwR3;)NH{Pu(XYJdz*#Gk7r{t0U}0U7#_&g#!!FrphEL-ywRuGK$94*!A4rI)Vo zhiF91{2fF3uQT~NaRX#Xm3nPU@i(~h>sS7-Ri)Q4tv&kXEFBV~WWS#EH|GCmDZW4{ zoUOCiOL@@;So1FZ{Vej@StRH`fBkzCiv zKh+eJx+~k|A%Sa)Y?{|VG4LMKMXEA~1g-SY`~w;M$#>!GXG^No>z@Bao^ERbrXtXL zYh3uBn2JNdEx=+e!cu@UCQHTmFgYE}XrPB!m5C@aHP@r#kW|5U`kWMhp@{ zVgJBXJY@xRM3i;s@joyXyafOmE)y22?f(Ir7)1e7(I--R^B>SV!U!-GW+Spaq5s4S z^j!o@Mfdx{#TX$t4hCU>3>z}#2hgGSCxKPMZnSi*DGCM z{I6H~1%EvH|Lc{22>8hNiL7bsL{pi?vC$}G?H#S8gL)Xf#6|883MUYxZ8gNnDr+Dkmq>8*B0&lVi+v;Xs+zIEq&l55uy2rBQ=&YLI2Wx zySdhS@ZW+0k^lyfjCfj$Dc{Vn8TVnlnlnxN0}i~rbb}*8KX{(WhKl(n;&0$t(+9w# zF8X}eX}4L`d~ANv9P0!;=#l#A>`O04Ioa>K{0&^!2230yukE!_(2Cww7{+Joo1aFC zPy$4bxONJhsa^e)yFE?7QJ&ny@bAEn{yKnCXCV!ZZ&}0|- zm{2suy+1s~n50ah&ui2;(a+>>NWc|2trIeB32WJmu_AWqlzT^o$xbBk5$9=l2lL1c z8OIi0lE3pxj1fhE9c5my=hi{UlG8~hWx(vIPe|+g>TH>L`@JFgxiGU!VKRRMku&N8 zSXZHUCZ)lnl9t&bjuEWv4@lrQmI$_JIV7*Uaq`>DzjZd*A7^`TtX&}{Z86RNLm1bu zeAbA|&=#jnJoEnziW$9u)Rsc8BaqT%wEX2Io&)GOg)#95)BgG_jQT%*+2sG6U%m!s zyY}l{06)(5f8G)PpMPy}Z9ns9qBfIb95}|zy0R1Jeco(w3>@|-Ta|=arQ1feRQ*)0 zT4@4*o`$G2?+@c6ECQIM)Rdlp{J&vW_v}v8yzQvrauO(G1^fQEBjRF6${TI|%7TwD z(qxTja;1FHQ{|RnMhqt)Q{qIGRaBxqrT0iB`!@{BwR=EVE#3Q`JgM#oquys_zD^5PDP~Mna^`DkXDu;nHMlM1 zTA|LRCTA7P&SwP3n&h4&TRjjr=n~riyT_5ZZ&&t2O>K1L@SCQfm<_R4%59;9R@Cb$uUW@E z5hXcRg?UAyg2E6%c9MblxM*UD7M=Svm&W9vhR?K*W{Jxef3sTZb$fSpy{7(+;PeBMDzr_)JG)n7iG(B{HK9@zV$PA{NgQ z05Uyu>lkPH3o@mAIDJ63UhgkEt^jl%!Au1wDOrU8Z^TZZpo8e)(}}UZnWqDEfSMT# zq`tS`Yd2-w#-{SLRxR&yszS_Mt;FuuwT%v?oH3<`bu0q-CuhvtKWo#Z?)T)WDH=2s z+A3nFUju>~4xoWOVHPokPk!b)&5HSnGGb|nifq{Oz?qh(p6|qcz-l19e5XM|Yt4)n zLSa&)T)Dwqq<#GxeVAoL5 zLviMbXoAPtcI&s35%}cPp2!X64FtZ9^tfPzQ^jj92azX7)Y}-Y!zO%l6TGfB#)y%k zU$))UN<$n~7@EOy-JmnRoz(LA9&;FPZMl@6&z&xIC(7==CE1vb2xq{5uUA=p?66r+ zXyV`Z9^*HM+XKnHaVBiwUns-(dsp|&mPFkh*7SC*Y77jX6p6wL z2$KC@6(%_&cv=e`geZJIe@xMlY|vXfJe!l0h*^7rVo@!9N>7MG-8<`om*PYs4i|Be zOuFWYTVvWd!XWoOA-p$3cg#aaA(4T~&-&^U^-QHC_NtzSv_X$NPNX=4HRWB#C}7g& zx(%Ml7-rHuUWhaP08L-M5%iwrCEph>5`#OEFhH49w1&XSSp|!x?n<&FK?M{zwl$ z`2IqYHz?1?+b9o`ZJNbm3EedG*5srWaJY{?H>lg>%+-XKcqcERq9rWh z-n%}F{ks&E+u0KfA*nhFg&Q)DaoW4d@HeUI4=DTcVz_UVg=T19XoVa+L#Hn}cEkyU zq!<)F9oK1O5#RwfVn{jonder{$o&51}g9S;JilaJ53)N~SBdN@YH1ACY38i|rPVEk7)f?9dByHXvW|h`=l&VJaN- zm3WcpB%rvu)e|IfRW9oUrh{1TTDs_mr<h( z46gKG``kiq6FOGMUPX>riKD(-+EorAp2@i&UHRUwxy}euz-t_j0JY(XFs8rI0|4KW zJnJ4~q@YMxRo)!b5SS~2nvL%g1GMUO@27`rFBTD@3MRQa?8St%fO2NQbATmyiUenV zU_+Q>pET_GaY1CfRC2;J;yJiiQ6(m+S52OK?fb_jhk?#7aS`$G&)k!t`U{F5SDDT` z*S#n+ZBJQ=Np@X{HCwLi+({*5$@z|}*1P$z#qi`f>Xl3Lk*#qVAe02{CYh=oO{00C zV6((ezQeJ<*cXzb{Er~*{kI_f+eo=Q=lMt`*|{N}DnL`j=+Ab*@IM zar&QH7w((m9(uw#ar1AaVNK?f8c=8Kl9Je5?-Y}2!3+9aEB23Ook_JhaC7}S`cbIc)SvRTfkL7toewt5yBi>brOk?M9i z4lWPJeInVMcgm}xmz7;_B|v?k#xy{xlAz*>iOM z40AB)?bdOY&(yXH7WPwlTxB3M{iuasT)^g>Lt1fN&zshiHs`ZQwz_9r2y5I89J^Z{Q*B6>FSr`J zpO19SHLvdvaO}vPBb9saC-PP4PUL*=a%#C{{-%V*`2iL_mF|M>?M3IM`^GxqRNcw? zK)cP|P9dB*_Y@ZOztZaT$2k%%4K&}q&TyYo1ekm zc6h#aQaSS(oOjSH!>0S-@s{==d$P)wV~j+XD`BE13N;D|&Ke(j(vzE+P|BIWnoRqU z`4l#BICp@`F7_IULomE-`}p#rg)Q8jJRi9(Adih5=<3No#!6$yEyT5wdUDB+w0k}K zKBkAi-xTVErk+pnEYK}|)F!cnKM#vvjUbudEUXSZr*TQXdD=Z?xT^c?Ri8d=r5nap zyUIL%g?CpYUr{m_beTy*;DeKQGSb3&+7DUg-E&6-=5n>&sOXE2i(_Js)2K=M;+y!7 zSc-X()6l6M1&-yDp!s>~LWcISA<%7o{a9%o^X(e{a&LLwxC;@)CTUTAUNNmf)8N3# z4~96`DY?`yZR^f_OMKbI#1ZtO(Dd74`P~suDW&*6NU^YO`ew3vc5CUl=pbFd3%JI2o2`clxpKq~fXz*n4Er&troym~BsyM0QJ5AyDy^sS0y^Wx*X( zs$(8uqM>&^*AAoMa}&~A;5H?`@g?Fm&KrG3V9$v_lGC2(+%c+gr)8nB%LkC<3BOmrzqPlgwXE~#^KhwBZ=r7;K_5cTVZ5XmqvtV~X4wT1d68tMVT zJUbsT=B&`qu6}5-!Dv!yt@R>#mr)2f++gU{xg$YcpLF<2l?ghz+Kw;%Dx|7Usx@_9 zG;QClz_PI~Omerv@$35@3Tpg;{@AC$@m`!dYdLDaaB`(+V=XrHuB1QqTv+t&kYO>x zaM25OkMW5PZE~UF)CWl!qvK}DPxH@@rIUCT)N69~af+?iN_pKKBsH5Grv5Mj7R`E@5kGRcfg5QDNaAL+@YhbVPAY^*g8DM}~2eq-O8&XI+p@J;bO zccr!N#-8LTq~6*?DGs9eMlr}c%_Tj=Fy(%y(uis+Sz)I_lU2a%7b0xDF$aUshaLoo&7?!@K9dedZt^35%asX{Sj$J71? zj$5-iju^&V%*!X4>Edq!)k)Q=Uz~yPcuN=tDv}Y2M&ScmWF6j?KDPW3i*&?g8tcn; zK8|_UHDQ2{ve!tgu}pU4so@T}*rmj~JLOH~Vo3JA0^(BZ%|T(PinMpu%K8GdOMxKT8b4K-FRp?!b36_})Kz}`MiRoXyPZis^*qUhcB9QmVYZOR~^AjXFJ}b{qC^5Hn0}CQV_fbCm4y@z2B7!)y8unNMJO z4$JO2%%Hi{CQqBB-1SSwQqbyv*PTEBMMB>U=dW&7!kyTgn5#`_fd>2#CKk27 zRO8$$2?u!1>N=G*!kC3pQ46c!Ipq^U$2) zBIbC(i)n^u_^1{8`aXLYEqd-Q-U({hLZ04t(EBcChk0X?d}*UsM&rp&^?GRTC(3H4 zg}Oxv7NOcZ4-1pBV?Wvm-ybifPVty)QF|ijj_tklM(1M}($OuTiK}AtJ_~eqRshc#Z@ZGeFz(I9l=+a#f zZ6GtyBpSug$`chH6mTwjNsEV#Qc0{qTS4$`A#{!+2}DxJDtS1dyjW}Hoa3P(pCFZ0 z*;!5k+zM|(mIOk7jhCW;KN`vdjRo_m>dJfC_nkwo5UFXcUev`5Ja!uUT2W@M#wbyQ zc1G*&rZLGVkt&PTP@lIGUa1X?&5kPV?MZ=yx6kF5Zw!h+*%Rp|7~}M{zN%|jzfKE+ zKTAQ_Wa?%sR4(tI7kBZcxxWFS3Qo|B<5FsSfsQ94tnw98Dn+v{4WpqRue4!YNmp@W zTf$_ipoophA++iI^dpjoVv|SlsdoyONr=;#%Xg74zm%g3%*P^e4=EeUH`3vkLvLIl z%oR8H_o&~1&2;AQTm(V#mRoxam?au65kr2KR?%BHcW@V(-^#4fXA=z~A+&jB>Pe9) z3l^(s<|U+q!IXs74|BFvFE9c&=IHLFaetp=ld)0mUKv+@K4}rKO8ytr=)46|>W~s? zeuywgBbcTjbVVF(U}_z5MvnC&3PLq^lrhqf`Wn4~!RK!g=TjO1v&VWRKKC!GB{KEy$jI>Sso;6t% zP~>K9b*FAWZDJLzPb|?fcrcXWWN9grPwn374vmTghvY7E#I`X_CcloL^MF{0i+7N1 z)cWY^i{vX?zPy4c-W{U3wpoPw{_>34LvYc>7hF0thvctz`%UM)&td^`m08sHe$G}%Z?`_y%F5!&RRdFQy!Y(>Lq4hA%1MuD2K9c7NjYE&tHs- zSC`={lyn{CfK+RI8>}D7ZBDslOifQC;^=RkXT(fq63cn3KyT=16EYk0{9`kJxj??A zU9$05pobKx2bbufG%OM(*wzHSn?&cKVu9kom19yYziLm&gK`oClhVf_&qji^cHHpG z|4M6NG;~r`q@weM@Ucb%sLp#TlyGcuA=gbo_PG@rV7jjEr=!Gvd9N|Js`pniGU{Q$ zbQ&;bY0^Tz7&iVOMXD{^adFdHx3X%$!}+bd%hYLO5=GJ;SQp>$!>=QryVccT68G<4 zU;n&0XLqyhriqpaVTos(o$}2gd9o%v)7Lw~rt`LTzpamSLi&3Vw#plD#B*`-h6J0V-ZA&4lNYuwegbC^zPi;pv3Q zVJA#!WM=Wym$#*^2(mnPRI`bDR_>cY?5hAKGI}zP>AS3Y4kKHbx~c3twtI8Foiy_K z106vr{S1ozGlmUV8@8ACd(m31J3+cmsZy5ng}m8lh6Z>pzu-{fO7HNy-K4mn33fxk zpx_>?UL+*N#Z6pglfz+rK&W2A6?u5JS`M=T`S{Y5&t&--&sGv1` zgj_yKJpGZRaVgNqgsXe?sFw|V?AZwI#wHL^U#_GCW{YmqF423&U#zLvJc}78Gom^~ zK@qhhxfO%F(tKi?cH?Xl!*MmlWgH!5EgeEmW(~@2cw z@$eSVZb2F>t_%#GEu}|@?%nK_+g$1foD7hm?I!Eb1@l~|<yo$K3jv-9i>;HZ>~uZIYuY#{6BkmalM0zJo;LhpS9(-7HaZ5EDOU6dOw zLTcmQUyr&FG&8k8Dx^C@@h>}ovmQ4xyh~b>w`Sck%ISYJL|J^6R8SnGLvPSm`vJ33 z%RSKAQ~cpo*~YlcDr^z;%ZsX*fa@ zp0s3Xk@!;z#0?GM)-fqM8v0&Az#}0A-gu$DZH5FZt5Bs|?c@+^1%0V%tTs@dH@CQX zOiR7fcSSLjxAo@sTA*3&>J=0i&5CuVfRlJ_L)Ee|i_9y`!o{f*iMVs&{A-(~7xhN3 zf($Te>b;8Tm1O;(p|WCnYtFI9gJJF*u5A@{ev=R3CkakL@Ga*(-P=o>gPLj0mu5?X zI!&)bYnt>bU4c<2gpzK{Jd3Gm5Zvf+OwYgDtQE_$uoZk!e6(+_jM^de;(cZz^27%* za8n7JyrRVmD691_SBolX!NOX>g)f2K^*|nMI9H#!8hhB$eR$B>$`s#8^Tdy`#tFkQ zU!A_}jwAjw&A3^eXYyDRupZonwL5jWd*(;m*!i+}*6J@WC7z|Peh;Fqx$_Ln;ZX{* zwrVn;_z89O`fJO*H@*04YQ80i+@}kG$4d9{f$mt%xF~HJrqg4OCBAgek#TxJ3`@`z zs>U2D0u6GTBS!-r96)kTg3ZsI&EKbN(z2B6G8nBW=3nVO4}#Xb+D+~$PfpR2P!6wb zV)#(D+;FhTuC$#~tZ0E)hgj{W5||Bi`I!CGIaBKGO@-briA0M#{S}@r z_EecUT}hK%T13P!~BND4`xTX?ZlmIxx18X;R1~V7awELpQE} zC|$72Aj)%ZD@W|0$(LGvpfQQO4sHSo6kUAHMOb3sNwQAog%rYcbVDaRL2Ep$MDIHM z5o-9CdI%++ySuC*X@rKQ>N91h(;l{$9ip?!GcLz|WgY{ioWa#DIJpM<2a$Ki_7K0< z?*`G=wH`?2NBWr)-=Jrps;_3ozA$D>jJ^YP&ZaV)26Jt1Znn?dlo#BL@5zOJG7SIn zrGfuh4gTstT+)m`vCvWwT?+-bH1p*^09D;@v{`K}lQuU$3&8utDc zP|g3?CGuBYqP|%qheieTP^(@Ns7C}HwMXAt2%U+c74yELTk{XUdFAPuJEG;GlM2w_ zc*tI{QDYrhmQQS>tKY;QCvn_)&@S9En`|4QOqgvo&3gM|AzZ zSC@QWV?22wG`o;J^ENRe0sdgc$u@h2d|T`KKuLElv?h)m&?w%4M&+tVb;d44p%TWp z9|-VpH%=xOS;CJN>mgG$WBnhEG7jR3w&<71N*3XA(tMCAE>$;+jEfXIf_EO%9nkGQ zz5b%h_N$Z5$D&TwFLyb0Kr8&$f8L~kbUp#*vt`xDR&T3OBFQ27nh2uu^$W0}R;$=f z)^sUa&Jp9?^#-ewQW;bqZi%3jU8JknTx9vI*56zQWBbjK>#?#oFk%JTk}DZ`C$wPh zI$ny6DCdv8u)1KSV_{&XW4W|;zPj9MVt*dnEp{k1z%V)4sLo7!L{1b@W5$cAaj66a z*79e8g63(;Ux$SiX!DX#i8m-4mMPhKbF9%-6%7wV1wSt|Na7uL6{k3@I(5O`)2oG;y%jtY%V4P!PMCYr`U z6{0clhviL^lCa}UtQdG_W#?mbd{yih{#%>cl_9Ip2oxD~btV-$_nB@Y*t`>E7k~cX zLrJnCCD(OMUa(M8DB6&qQC^c6=5`*rbx=W>r5Pe`^^}I++LXP_Ss-@wYbX$1wp$yX z8`zi{6FLY-L)J=i7hGOEOQZ@Jd-gJDa-8PdhTUej>t?4Ef{T`kq`sD5{S&x2_g#hE z@g4UW-2o}WtK^n>sPhbymcR~<+J4j%ToYYMm%Yn)xG+RK^`1%~M2 za(EzAHUoL-e0Bo762;`;>jd@VhrUAdD9PQ*XkMAkb}dM^kTkN<@Wb}zXwM;SAPw=# zM<=ek<(|}f9V|g(LVC;ACEqq zo_L@&kQ6XicT5r@c&`GPCaBtLGZ1#idMv09L(bN`aED1TShM;1D2Ma1^%qmsGlmh8DvYb%juJYGoQSSekI2wi&R- zvYjrdSSG)0RR|1n79T6E(L=Gnuxw$NRYgpvSvk}k^QfWPqy5iJ$}D>p3eQt~aDS52 zH)fTrN4CL*F447JzRnr9G1^3y?Na^drvZq!PHvITVy(w*qaf5{-2tl*9kJS2Y&mlg zyhVw^KSSFuZ5k-tJ16M9p?9tFNyO53kBqEV)~0-De*=Jw!BGk*L()k(7dmv(K(~AqibH-{hCvEj^XN1G!9k zy^2zk12>DJwOL+3QPW`UB?*X?zj-@J0==8XTTa7DjL1+nS6=cZSLYNn{LxUZ-gvps zWhE!iVy@gr2@RoHl>s?$^`W1#!*)X1L*UxmVL!xz##|?AsYhPEm9#SXd(OUMz3F4C zXe3rzmhU>wlmGpj^j*FMvZQVPFD>gV-xK=Rk+67{{mqE}{ky9wy_`{(m;W+m`*vxI zu3mkDj(V?Nu}6G;Xjs{dvGCgPdiLw{sVLhbChQ=G&v>)UrKEjbPI}kZZz)m95u;2j zHc*rCTBL18ZplT@P~ML`bQVc@>SJTYexU5!!rRHt`)WH0aUoMXeMfaeEnD8IO$^r* z*c)fbxZI!EPKcBzMQb_-^G98JyE@@2mn21BU@!-s+Aq177|L(NM+a7Vm51W;#lKp0 zk4h2(RJZY+*~V~B$m)%k4fUQD5+Kd|cjIdZHxlc_*Xo>(I!MeKy~*x&8>J`Wz5|;Z zhb@v639h=|X6}M7+)TJsSzLK74&vGVSY%H8`cH^j&}}wdeyz57z_!{xNOuHd7J+Cc z20kw<`f+}kd;Y1VT1Z3RuvDFw#Kc+JhBSNu1&~k5_#O`ZG*Y#n zEb}Dj0Cp2}kCLC%)qBEyf@oH{CFJ{)TUy z>Mb}f=9>fhbf9~-#ut;EEp;?(#g5Bwr1Mpu!MQtD6^xd1RAa_hsk{3%16{-|*rkvU zC^MYO^Rx`;s|r41oW}Ea^(Hb#T}^H#mFO~Rr|SKbn_hZ6m*8C>4I_r@zmA$0c|7)N zNrsj3%D^g+`@q##)u%YNtT(hT(-J$>RP-hlsfRDbaM6i)CvNRaz7kd44F`CU#7y}u z7_i&$NCt{@t_C6{#*j*thCg4s{$1^0u5%Xp(nU@8o~yu(F=~B|XbR_S6%+oKo)!`XQ<58s3-PX`(tx7cTwGu2EIj)@m6k*Q8t6O~b!T zTo+EP7G8rUSniOww=k&~v5c zJ-=E1;LE6w8j$V#E~7E`P;F|Z(uA@*(AkbVkIld>s&_XyG#~ zUGBxCZQ`QgZDIDc)I8Us**Nfpm%4OToloe-*wu9dpHWZbo$!;gSTRb*I(uxUWLMU%Rn(L4cJ31@MyfkF4*j z-AulgHU(+g4fFy;*d6`QcCyY8Cq!AlH57(}st$vo#+p@E!M$C9@n4Zsm3R{!wm7yp zm0yxg_ttLGN+|og>U_yFof3}{(sF4}=1cD4*J!c1!Z{%gQ<3e8wR?6xs?EcPjB2=8 z=W#Gq7ngfi_2z6B_Zg-8y_!!p0~?)=vjT9@;fKz zx~83(x&AYAC-3C;)%ESiN!_)tMUh*oJ(~s50%2q70yWjQJ{f<2>BBk)QRO*N7DA(_ zbcCn=?b)se8s*uIiHtMA8-s5s13X_OjPC~{O3SKoc-bF2apBt+S2FVK6E9eDf3x2< z3T->S`0TQV^hS5UPcgOIFoc+G@~?-@SQ}Lc1`hd9{U@UA=`L&r+SziU)e?ZY`#6>A z-QyPPcgo3mx`yPiJWwfEL^kj%WvXHtPP!|LUU3avt-;VwB-Nb1eX*bq?PuRqv6ZPI z$!BpzPt1v37R_CQjE_*Pwgz{(Al^%y_Uqs98oIFf4oZ*hE;{ zxEN7fY&RcV;2lG3V^ac`d9u8u%8#>e_y!3tz_Tl0XPronj4)q7nBmIa zRkc@sH4oP&$1oH4OvfuUKYmmVX#8@; zjcy3hmbb2;Ut?J%mt}SdxzXqK@ytb&O_hmP7s;cQ_3~d$N6ttNj-!oF0g|(UwGTtI z9YESjhInO)4;MEFCUc7+A=2rz-f`)9zQ}@8@jT-b<<508#tqC{9EtIz6_kQGAOO{m zuAdpJw_mKLR9}LoSbkeC3<%3fKu-A>+1~9MNaGI?1Yk?K0e}_+@%GWyZmrH7-rRp| z7Lp_998YErR)sHRIva7(LIl{8FRDe_j?RTTrh31*h+oW`)qEBeUN%@$>%d+$m!j_Z z{SNAUHyL&HE#ELw4))`^h@1TUo0{R%9#AgNcK2Cs>@4kjMVe3{=yP~)BfDog${YZv z#TjUt`UhxgR~t4d^Sl>Jl+UGnhDXXd`v;XDUs{QDs%{QEb(3f%=ou{i+1i236%+>J z`NCtDk6G7*UO-92K||O!FMbs0e<}C;WL{N07$HlKx4qbLB5hDYDR2qbOF!P8$=`%9Y*m1Pzqhc+rY)Wwj= z3%qcEAVjnklY;ZCnVkrl)1hO?jgeH8>-S|;PzGN`4hrsR8;UH}QRU%T)62mC5pb>A zyqvjHw9*-ygUHk)MhLh6sm=X=|7BuIkG8&86kO8~nDrxn_wd84R`J`|?g6Nasl+C< z8VJToHbp?*c($ztP&*H8@K>@YBm{iJMqg9YY^)sXf#xKKZt17HHJzR)= z`goIGFI zgRs2I^f{GiXkfU@*!pu9s>?G--tJKaG{pM6z;;D<9s5C~3XQ6K=DuhYwfLhVw3+Ke z21K{qVHDOB|DkuNc<_6NSk43;@hHpY=7;#$>Y)n1p0O2f8Q!abXl2@fGEUa1KO+}j zRK#Q6Q=H0I!a`6Lz;?37yB1iXT~TV#)d|6i5U*L;)Yjj<3E$(#WH!9JmDhiZ4mI}G z&SOVv9jeWq9IZ>Gai;UyY~ET$PTf+Jyo^@xG|H5nU{xc;+%sW-50HmwA*1H*OjI(mSWz25uH%reI2Xjqcm_;*Bum`0MXTS8V z&Pbzox0O~#-d7HDR9c-$NVYiyY6@D(|6(XBJAYtJO&&<)D#}RD+K~l@IeLd3ONm=3 zl4VCjz5R2icKvy`c2A$tni7N)=Ep7eMfC7-9W}D3&(KESST{4le@@|?e4WgvRMAf2 ziwen9tUJ(KlA`-re@zOpugcK|z*5 z?t>#IlAP^DMQFGU- ztrDjWIrs682G}mnA=RWq35_k3FlZ)nmMwRmi>*k z)4V{P&HIp^8c1f@Gy24@ovtt+GZs7~XZO!y<3gYF_;Z7cnQKme^pTwH&y@G4FJ@FSini$_{S!m~wXCIcFqy z24XS5&>sGQ+URRn@4Zp^nMY=MXQ+FGVKVydBf|B`69RGl{V9tzDM5pJ0Nt6P0&HNa9BDH9ZHi(wUi8}akMn3d|?QEWE z&W4&pen6^C`G@tx8bS*877OgDW#%ja`QlX7p^T)u&^A*yt*zN?9H0wwSg(IY9D6A7_{Q(i$ctm6Z2hc%Vz1S)T{aBvFMjQ|b*8ir|U z3-?AyIb(}%NRmke%jl3y&$Li0cnlK(c$~3=eqL}FSkM7s{hW#PQag0*=D-bT2(BJD z930(zqG|R;$+^Ov;vcw>daP}I#Y!#!T;;1l^)s`iE7l$cUgFfnp+^~Y^C4IlqQ%Aj zw7a4442~p0i;K6P*$|tp6?ImD`Qq9p@P!8^Pui7Skw?v`=K~$KTL{3>X@)OwoFngy z60Atpw={r7GuEg4pq9%C8CG+G01DFYu`fO{BI+i}+S9D7LjD*K;-7!3!1@!6__?FF zM-}(G<6mvRyZfoy%7Ri)f~T9dw+bjO5_5*x=>o?J%~7!-!J)t8ndN0D)k`{S6{&3e zq9IeGQFLnpJ3<3c%N@oGq){{(cG4M2uGf?8V*43RCW6Pq_->&4)?+~gb>3LES z_UP$%nHk6Cl|J&dSvR_s!@Z75H z$JHY^Qw06QSd{RArr@Y|THlZU)X)L?Re!>lA!U|{; zav`=xh24p|z>0dKIOoN+Qe}86royQvrOLFO{5ga&&q^;U^brh$JGHMi1NWm z_Y}?r!4oB`M?M2Hyz&)uesy*f7O|Eo5*bOd=EfY^B4Df*Hw}BF*-uhWD^NCbzL|z% zHT%+CL~F-dUmJ`gi&N2$$0^&piBt0XF!P}nmsG{>*kxYU5G*Z3($^R_C++(7jS7tO z}{(Q*?%s<3+4A78eZ^qg2GR9cqlEL;_~s_6YhYR1!+w|=T;4JkOBC?1&-J$z z;JK`KlR%oR>w99G=E{9s6XA$%a%Z4x{0OLGndhp6GbHIoYSdJOD(LHn}Od)Gob;pkBzJ|Xh4~lT~pHL)TGcj zMUY0QbtFT-hS52f$o0R+N7P4T3 z*A}O%mCjl&R1mxG+A70eSVE?!JC?_=#}MA(+aoKu3Pg&V1rtg47SM)oP8vpCyo(1U zo^-zyx$ZeRst*3lbqr|B8kt&d92BZDkk)&UfP>pAeM?ZP!Ag=c2K-Q>$suyJKLnx1 z*o^&R=u3C=9~Qe)`AtOTFPe0?MU_04>0auqXUelny^yAXjpOzIYJ&2%%699WGPBTf z+bqd<1l&274|(PYD-RmxAw5WMquTopwmqWYb;t@RXT@}|NE+|P9g$aH-} zCAT1m$%h_)i+on({;N~(!R?6X(bZ@_rGIZ$^}MJ75vHA;mv`2&z=1k*>uzhQlaZ)1 zYGbmtkCt|x$M}}(Tk*GD2g}EZe3wW+D1EjsM6K}3KxQm>G&*KFw4lqy&{BKe>YK!d z=KUb+iXgKO50uIaex;|_D%i({MtK@&VRSA45eq%>VO--I*Tcx}S~~oi$(L646R%zF z)V!~SH@gg(DBP`*3>_XgimFzl^-;n_g(7A$Vk5SEi(3n4rnM~9gVZK|`1fA6 zB3e%A)LDHbjlib6&AwZ3yhgOpqLu!)BS=mRDc$|y!$iD%S z@yIS#TQk&CSF(Xxk11P9+s$*Eo(E!1J$70-6@e|#)x%a0o+Y8&^( zlORAHideJP|M%eOucG{h7hLUj(Ja9;?dt8R6Hkf~9q56-gMR}u^r9=|yLBIImpmW} zQnyCVpSkKV>$P71eW-_;^3vP7V9A5``p%-oH$AbS0WYtS&ZQaI@)=7y4MTVO`f2?m z#)QijGF##63)OY}C~$n^w~m`ji(kiP}t6qU?(e_zKyW@ z#Bg(6-_aqfE@R{7F`>&n1m@gEcF3u~kIP(x2Cwt$2l(C~`z!?w$@g7h`aA7*8LHAn zxfJbR?nYY~8^}1vcels<(Dk8^B(b-L+>J5L9^a4`Z^N^CVl9r2W%ztxp>=nvm5CxN zT&=3N*L1<8>ZK07d_6+x`sTBX8zF>v_)@o%&)ED$vSwNy|N9>;)0PPdyse=>zI+Hw zHgM}cASiLwPmO)Xg;vX|FDt*HEG11`79lRzwwk`g3YI04K{2fQdZ3+al^fUOA3;N( ziF)G)PHxqNn}Vjj!R%EiEwEGiK~yT0BCt6-=SdE!7HfT6aiC{4Ls4c;9u4Jn8rZ-? zL(#7%>eiaUIcn)#HZHRi@hyF~ZjJF}NP43qL2vNsM@Z=6HEOXYH1dO4MGZ8n?ey;VW+UR$^${Ppbx%lJgcRaQcpG~|k( zb2<%x3?X+`73g_%q=>8lG7%%W$T|-o=H-$nYRIM~c!(NS#>H>n=vkD*r>wy`7Iu#^*daL@dsoHun*})y@RC zjT%0`cMkZc`uy*|9&c~C0l?N`B~U6^Ppw3`i4a9YExNU9rUw8Q#rAw%fwh%e(!W(h zHnQzMrl5lEa(qkKUpFy`+bJ#_vTwYPQppA-cdfpwDt)#i38tq)#bt7d1r;t&kj-T} zG*0$QF72&R|G~mJ4D-|TH8k0u&B`vH5{qeK?)CmGVN&e15h8pffjnpuXB_w|(0;i& zRc-JkoRtHgDT@$=lN7HG!k;Qhs-VfPX?WLVtC!<=52m0z{L35DUd=Vq_DhX+&wYKP zuo2#v4FySzX~M@^(Z(lsP?hyCU1vcjf{V+#=D^l4j6P30)BctE5Mlw4E8A(HFpc9P1 z#`Ehgzt!JiZIiL{ztff9`3_wEUd;O9^EX}Rj>-caElTciFd$Jt9>A%SJ1f^z)N%WX zx@zv{sqm_TR5Mt-KTv#Tea`|)niVDkP@>q`7Y|j~HtTUU>v7Yy)BCntTMf>VUo4Mz zyBzwFW13TToy|{EBeSsoyx>do;s<@~$9enTZL^+K0K?ZBDRXo^evq~FQSZ-DJ8E?h;z=(Xd(Q3_$DHF;?PO7n5*}*jBzZ;zO`-DZr zQJ@hjxt_ItC)ZXtedC#@l+cR{H`N$TeVxy20u^zup`{y*p>?n4#xBU>X%PZ25%_9l zZ}gn_>|AuN`pnt2Q?{ER2HmqZc6+^k*h=BI6;IXW!&FAO9_-)+=ZdU!+C3J+RMBar zl&$8z`h>de)Jo(&(oXHFf7fP~6wvOxC1hrcm=isg@tm*KRdx)1a(6vayv^T2R;6}C zw(w>)N?w^p@TCjXzEJ7~mz2s=A*PZVSnaWdrN0|E{D0?yA)58n+h^SC=>+IpuF=kq zdX&`8Fq%u4QJ%h|&cNAjEnJ}OC>U)TD;_z&@L7tYpWdc4>{m3r9>*V5`wSH=D-tNw(?Jp%co)a6j;bF2icJ9%q~2C43GJT`+@e5 zz%5&^02V&3I09V6a{Q*~KqV;e6)Y7y0R_iglw|=DVYHYC^q_C1Z8*V zk@U`4I!eicu=^wD9&#fJj0`#p`qCBgC%zp2YtrT4N(ur82s|Jl>tE2URG8=RaEI$I zyZ0fmY_%&hu~de%QHsN%2Y*gu`v>`MumSXKc5E?<7MxpdT`GcEA5##zeGH&?%~YNG)qx`D<$8-|lEX z6HEe--KKPTf7uPvG!)D)q9M# zWV(NC{@Cw}IL{Vd`J$`vBO}YD~y>|G-Rn<($IJWx;jJct97m&PCya*A4PF5 zeta>{7EdFvme5@4D$b>vZWoWjX8EpMw)unv_SpXbQR@XpzS5?4a*g=XI)7F12|RCC z(Oh3ybn}lj{ll*}9Rkevw2R!oDZzhkpGgI*;m}rg-O-<0r$4Fj6!0Njz4d`TnG@fIL-0GpWpoSL&MOn`csUi4)1kA<^VzOsSy56 z4*o+91da=^QZ?z_r_TR$9e=)@8j6{swc6d~l5-L~Ydy^s_DZPlSB?eD(&kZ%0VzVU zVKO{9R%xRf@AD2n{$^i#{z?nHxyn|T(Clidf4TmCRGpje&c~JaMen-#_#~32y-H!O0Y?rn z6Zo41C}#hzw!aHNgA4Pjv@>_J+jy2tg0>U?E|q+FPQm}p4E;f0JIMvo^swH@;w1wi zRu>sv7v-evJKNIy(aHR=n^v}2R_Qep1?%sVm)0DttuacGb83nFJtrwkce0zt&8#K; zM6TOc;XB+b3R`kqIO0D8VT1N#$H*s~d(|$$Igty`RM?|17dJD(&d5XzyD6&p`i}GO zL`)a#O-zfG5FYK~QSGm$ub)jlBUKs_?6}jBM0lOF-cPt@EX^^TGL|Ldw5|}<{i*_> z;EArPJ+4Uo`&%YRyE`Cbw|Js&x}?l!DK|SS8)gNx(HSopUcSyj5|){saKCgt^5k}y z%V>j~4aE*jSI{ORQ(H3YJS>?B<1}q;?Afak^^YBSOJskp{(l-BSfi%Ac7B*l5K+=^G9>|RJw_daHK>}mGJZj4hJd^z2 zJN`~vcCTqa8K5%nET?cu1KU5~{`Sj=R~K4R`||+>MQPa|C2TRDhrQz-f|V1N&O68d z;?!pwtm-)+Cqkj@S>g{Pno{bk1D6Rxp(h1PzuxQlQCfYrOCroMt9&2}T~|%afdJOP zH;F^6ya&e-t>s6r>h=qB)W&AXC?RE~0l>i9^yO12V+^(wmF3nhYRgzI(qCY&+bd3O zNUfRAv8)_|FgcER4Sf@T?6__;&>=qOXE$@7m9RADcN7`I{390vZsIcO2l8dqFvGi?soUm zcdRrDOfnHjJ@d?lZ?r(O+f05R{;}PBRD#l#i88mf2-{X1V#$aD=ZDSP+;e0emApOi3 zut(-j4o=QwJ79R1QEq`5<}K9wUtQ1t@Dri?eKl2ynhG6)G~Q=rWhLO);{Z|Vz_F)l zw(`5&`#Q!H1>g4$r$Uq!DuJ#tF~vjiM7 z*_*jpXlEuB_k!a+9lr{!-^RZ+s%kas7N0JfjdBtv8Z|h?>g9z!LVP!tz{AMWbc$*o?22Pf-EVT6(ig>9Q4r+0LmO4k*5q#>J z)4gUq+hQ^yvCYHL0pr-YUYlbz9Qr*2tZ&?COAGgd+roH%wTy&G z_+L#nIzjBB`d)!l;s(Bn&pY?dS%3&9M;s&WsXUQ$0Yy%(3|tOae$*Bay*Hz-EXOph zP_k}j%ks%JLhc5)EH5jnRso#-HUFN1J~v>hx+SLZZ4Nmj_E%jRoyMxj{Ip}-Vj5+g zMcsY$>e&wr)ND2%A=AV*#=X2wvF$3)(#a$=_~dyv(JTz9M|?{69J;XMY+lkd-lw&j zKm802f42`^L|Ii7aR-g>)si(_W-_?pzFuu5^6pY=i=5^q9A_ zk?MVE%=!b9sC}fV<=X11azQMhXX%oAe2=6Qsek%F;*#>o`{rBz5i^(X_g$PsnozDd z`z}c=6QghgD^j`qwiv90LIdO~y}3f|mFP2XGhy`64fdBgZUU`juYAY|1`_$zhpA^jP!#;PB6& zA~^tZ_f;`+cEMl$U~mtjM97{y7Q&Vm=O;9qlrrY!CFK*pk@-&aEZM(K1ACSyBBo#b zyv@b&o~KQ^>Jn4?TK_&~3d-TwznIbm( zgYMycFd~Ab-t*HZ1dF`FiTV314u-_R;De6c<<@w;(TU)*PSV;$CWk2RdFq=9F&}hg zR2z>>gvf={6~{RWdPN|9FcJEY51YH3nkW{DVt<2v@{RP#{9rzMWgea=+*%C@9Qq(v z<`0AIEdu#|<+%B$A~}{4Zk4FJE@AgO90t^M4H^ohp_XbM)$#lKQ84KT`~tH%M%_Jq zUXYw&R7po&YzA1oo23oese1{Hwv7%j$nBb}^^UW%upF3s1!{SH+%nz4VNHbpX)^Uh z#FXxyxYqFGxH(H>+mZ!A93Op_C1N1nhkl?|DE1ra4Im0i7|K9l>zz5uM@k80HDE}l z!iD#~Ha2nc()@L~;nU*$r_A^`#O`4yEp6cs@E^u)X6->!(~I~eZ>RD)D0tOmucm7K z;9K?<(n9Z5(_ZV6@EX$0K(JZqq^VFM=kw-{OY;vw){BF^QWH|kcPZm=P;>8W&Y--1 zhn!Wr`st~-M4k$ea8nE3kJWdOYgR!-`yKS9yP49m4Qnf*a~ZO zUFKP88EG|svsY3Iv>p66-qJEdOf0Ef^by7pz%2s1%UM$L3cKkCzUW$4HTye{HMG=K zUp4bdr(3UBx`=bmf{_kw#?yv-V!%Gc@^NRVP{cs=;l#1;W?c?ABWv5owm3hPA0?gj z8064cCJx)p=Iv0oy^D+bVl|p+_e*=-Rt4H-SXq_lmC; z^YXiC-tVxGQq-L-NpdiS%^xm`D{q1OeVXdJ1!rML_SC%cYr0RF2p|}n&G}KL`pj8+ zGCe8vRCzUBAHUXiqr2xJlX--_gVaY6(`a>+=NPaLt|FAqJjdo*o ziSMZujO`qw;ab`UW63SN4_);kAY=RyPo~t(tueNl7^yMuqKzKGiXH~Tg3o~qr*?{6{i5`~9o_xSSsIC3Q&R`pg zD$U1Q@`79st*1HBfaXy587^FtA^|e_uguoZWK8`+dnIf@2x+jtsKeRH)3M_8P+vW! z0DtBEMs4DIQTLvS?J#O@atMTXX=+uBIA();U@}wW2^F4(DEU1X5BQ?l zwL-s2TC%$9z{Oo4YRGr3VBA6kFI;>- z-eT>dp&P2PcZrs)b=A!_AfFpv+~i1_yMq7brsa%BFIan_@bGVYOSmDHk6tq>&K%f4g&+YL|-YxaY zz+Ml{Q4dWTXW1K#z4?At`iMwYOGzH?C(@ya*y}F1dnHz9MuN!s@+3KBZq15W*c9M` zhTK0Yu1F{v*^q=@vJ9`{eus6iOmf?*Gf1c1aoww1ewo?u(2LJX=#-3SZ~jqDpN}M z>`k71+8bW4_@k8J5PkR=gzG-pA5(4nc1J+5XTrXCQ4YTk_`9WLcms3Y?(zQty7U~T literal 0 HcmV?d00001 diff --git a/docs/user/security/images/tutorial-secure-access-example-1-space.png b/docs/user/security/images/tutorial-secure-access-example-1-space.png new file mode 100644 index 0000000000000000000000000000000000000000..a48fdeaa6efa1f6867395868bd47c7046845b95b GIT binary patch literal 328089 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�?Q{_n~Eb*TP#SpG-j`Trf3=ruU1A+QK*FeZ8J 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