From a40e58e898d1126fa361688a2eb1ca484963f2e9 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 30 Jun 2020 10:59:14 +0200 Subject: [PATCH 01/19] [Discover] Deangularize Skip to bottom button (#69811) Co-authored-by: Elastic Machine --- .../public/application/_discover.scss | 12 +---- .../public/application/angular/discover.html | 15 +----- .../public/application/angular/discover.js | 27 ++++++---- .../components/skip_bottom_button/index.ts | 21 ++++++++ .../skip_bottom_button.test.tsx | 41 ++++++++++++++ .../skip_bottom_button/skip_bottom_button.tsx | 53 +++++++++++++++++++ .../skip_bottom_button_directive.ts | 23 ++++++++ .../discover/public/get_inner_angular.ts | 2 + 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/index.ts create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index b0f3dfaf96c4f..1aaa0a24357ed 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -100,16 +100,6 @@ discover-app { .dscSkipButton { position: absolute; - left: -10000px; + right: $euiSizeM; top: $euiSizeXS; - width: 1px; - height: 1px; - overflow: hidden; - - &:focus { - left: initial; - right: $euiSize; - width: auto; - height: auto; - } } diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index 022c39afff27f..3c16e4a6d9dee 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -65,18 +65,7 @@

{{screenTitle}}

- + {{screenTitle}} on-remove-column="removeColumn" > - +
{ + bottomMarker.focus(); + // The anchor tag is not technically empty (it's a hack to make Safari scroll) + // so the browser will show a highlight: remove the focus once scrolled + $timeout(() => { + bottomMarker.blur(); + }, 0); + }, 0); + }; + $scope.newQuery = function () { history.push('/'); }; @@ -1007,17 +1023,6 @@ function discoverController( $window.scrollTo(0, 0); }; - $scope.scrollToBottom = function () { - // delay scrolling to after the rows have been rendered - $timeout(() => { - $element.find('#discoverBottomMarker').focus(); - }, 0); - }; - - $scope.showAllRows = function () { - $scope.minimumVisibleRows = $scope.hits; - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts new file mode 100644 index 0000000000000..2feaa35e0d61f --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SkipBottomButton } from './skip_bottom_button'; +export { createSkipBottomButtonDirective } from './skip_bottom_button_directive'; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx new file mode 100644 index 0000000000000..bf417f9f1890b --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { SkipBottomButton, SkipBottomButtonProps } from './skip_bottom_button'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('Skip to Bottom Button', function () { + let props: SkipBottomButtonProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + onClick: jest.fn(), + }; + }); + + it('should be clickable', function () { + component = mountWithIntl(); + component.simulate('click'); + expect(props.onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx new file mode 100644 index 0000000000000..ccf05ca031a8d --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiSkipLink } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +export interface SkipBottomButtonProps { + /** + * Action to perform on click + */ + onClick: () => void; +} + +export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { + return ( + + { + // prevent the anchor to reload the page on click + event.preventDefault(); + // The destinationId prop cannot be leveraged here as the table needs + // to be updated first (angular logic) + onClick(); + }} + className="dscSkipButton" + destinationId="" + > + + + + ); +} diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts new file mode 100644 index 0000000000000..27f17b25fd447 --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SkipBottomButton } from './skip_bottom_button'; + +export function createSkipBottomButtonDirective(reactDirective: any) { + return reactDirective(SkipBottomButton, [['onClick', { watchDepth: 'reference' }]]); +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 2b4705645cfcc..05513eef93624 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -63,6 +63,7 @@ import { createLoadingSpinnerDirective } from '././application/components/loadin import { createTimechartHeaderDirective } from './application/components/timechart_header'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; +import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -155,6 +156,7 @@ export function initializeInnerAngularModule( .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverSidebar', createDiscoverSidebarDirective) + .directive('skipBottomButton', createSkipBottomButtonDirective) .directive('hitsCounter', createHitsCounterDirective) .directive('loadingSpinner', createLoadingSpinnerDirective) .directive('timechartHeader', createTimechartHeaderDirective) From ceb8595151768601f5257ec8d7bb2163328acf59 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 30 Jun 2020 10:28:54 +0100 Subject: [PATCH 02/19] [Logs UI] [Alerting] "Group by" functionality (#68250) - Add "group by" functionality to logs alerts --- .../infra/common/alerting/logs/types.ts | 103 +++- .../utils/elasticsearch_runtime_types.ts | 18 + .../logs/expression_editor/editor.tsx | 26 +- .../alerting/logs/log_threshold_alert_type.ts | 2 +- .../group_by_expression.tsx | 85 +++ .../shared/group_by_expression/selector.tsx | 56 ++ .../lib/adapters/framework/adapter_types.ts | 1 + .../log_threshold_executor.test.ts | 572 ++++++++++++------ .../log_threshold/log_threshold_executor.ts | 292 +++++++-- .../register_log_threshold_alert_type.ts | 9 + 10 files changed, 918 insertions(+), 246 deletions(-) create mode 100644 x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/group_by_expression.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/types.ts index cbfffbfd8f940..884a813d74c86 100644 --- a/x-pack/plugins/infra/common/alerting/logs/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; +import * as rt from 'io-ts'; +import { commonSearchSuccessResponseFieldsRT } from '../../utils/elasticsearch_runtime_types'; export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; @@ -20,6 +22,19 @@ export enum Comparator { NOT_MATCH_PHRASE = 'does not match phrase', } +const ComparatorRT = rt.keyof({ + [Comparator.GT]: null, + [Comparator.GT_OR_EQ]: null, + [Comparator.LT]: null, + [Comparator.LT_OR_EQ]: null, + [Comparator.EQ]: null, + [Comparator.NOT_EQ]: null, + [Comparator.MATCH]: null, + [Comparator.NOT_MATCH]: null, + [Comparator.MATCH_PHRASE]: null, + [Comparator.NOT_MATCH_PHRASE]: null, +}); + // Maps our comparators to i18n strings, some comparators have more specific wording // depending on the field type the comparator is being used with. export const ComparatorToi18nMap = { @@ -74,22 +89,78 @@ export enum AlertStates { ERROR, } -export interface DocumentCount { - comparator: Comparator; - value: number; -} +const DocumentCountRT = rt.type({ + comparator: ComparatorRT, + value: rt.number, +}); -export interface Criterion { - field: string; - comparator: Comparator; - value: string | number; -} +export type DocumentCount = rt.TypeOf; -export interface LogDocumentCountAlertParams { - count: DocumentCount; - criteria: Criterion[]; - timeUnit: 's' | 'm' | 'h' | 'd'; - timeSize: number; -} +const CriterionRT = rt.type({ + field: rt.string, + comparator: ComparatorRT, + value: rt.union([rt.string, rt.number]), +}); + +export type Criterion = rt.TypeOf; + +const TimeUnitRT = rt.union([rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d')]); +export type TimeUnit = rt.TypeOf; + +export const LogDocumentCountAlertParamsRT = rt.intersection([ + rt.type({ + count: DocumentCountRT, + criteria: rt.array(CriterionRT), + timeUnit: TimeUnitRT, + timeSize: rt.number, + }), + rt.partial({ + groupBy: rt.array(rt.string), + }), +]); + +export type LogDocumentCountAlertParams = rt.TypeOf; + +export const UngroupedSearchQueryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + total: rt.type({ + value: rt.number, + }), + }), + }), +]); + +export type UngroupedSearchQueryResponse = rt.TypeOf; + +export const GroupedSearchQueryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + aggregations: rt.type({ + groups: rt.intersection([ + rt.type({ + buckets: rt.array( + rt.type({ + key: rt.record(rt.string, rt.string), + doc_count: rt.number, + filtered_results: rt.type({ + doc_count: rt.number, + }), + }) + ), + }), + rt.partial({ + after_key: rt.record(rt.string, rt.string), + }), + ]), + }), + hits: rt.type({ + total: rt.type({ + value: rt.number, + }), + }), + }), +]); -export type TimeUnit = 's' | 'm' | 'h' | 'd'; +export type GroupedSearchQueryResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts b/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts new file mode 100644 index 0000000000000..a48c65d648b25 --- /dev/null +++ b/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const commonSearchSuccessResponseFieldsRT = rt.type({ + _shards: rt.type({ + total: rt.number, + successful: rt.number, + skipped: rt.number, + failed: rt.number, + }), + timed_out: rt.boolean, + took: rt.number, +}); diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index 9e4e78ca392fd..295e60552cce5 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -22,6 +22,7 @@ import { DocumentCount } from './document_count'; import { Criteria } from './criteria'; import { useSourceId } from '../../../../containers/source_id'; import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source'; +import { GroupByExpression } from '../../shared/group_by_expression/group_by_expression'; export interface ExpressionCriteria { field?: string; @@ -121,7 +122,6 @@ export const Editor: React.FC = (props) => { const { setAlertParams, alertParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); - useMount(() => { for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) { setAlertParams(key, value); @@ -140,6 +140,17 @@ export const Editor: React.FC = (props) => { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); + const groupByFields = useMemo(() => { + if (sourceStatus?.logIndexFields) { + return sourceStatus.logIndexFields.filter((field) => { + return field.type === 'string' && field.aggregatable; + }); + } else { + return []; + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [sourceStatus]); + const updateCount = useCallback( (countParams) => { const nextCountParams = { ...alertParams.count, ...countParams }; @@ -172,6 +183,13 @@ export const Editor: React.FC = (props) => { [setAlertParams] ); + const updateGroupBy = useCallback( + (groups: string[]) => { + setAlertParams('groupBy', groups); + }, + [setAlertParams] + ); + const addCriterion = useCallback(() => { const nextCriteria = alertParams?.criteria ? [...alertParams.criteria, DEFAULT_CRITERIA] @@ -219,6 +237,12 @@ export const Editor: React.FC = (props) => { errors={errors as { [key: string]: string[] }} /> + +
void; + label?: string; +} + +const DEFAULT_GROUP_BY_LABEL = i18n.translate('xpack.infra.alerting.alertFlyout.groupByLabel', { + defaultMessage: 'Group By', +}); + +const EVERYTHING_PLACEHOLDER = i18n.translate( + 'xpack.infra.alerting.alertFlyout.groupBy.placeholder', + { + defaultMessage: 'Nothing (ungrouped)', + } +); + +export const GroupByExpression: React.FC = ({ + selectedGroups = [], + fields, + label, + onChange, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const expressionValue = useMemo(() => { + return selectedGroups.length > 0 ? selectedGroups.join(', ') : EVERYTHING_PLACEHOLDER; + }, [selectedGroups]); + + const labelProp = label ?? DEFAULT_GROUP_BY_LABEL; + + return ( + + + setIsPopoverOpen(true)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {labelProp} + +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx b/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx new file mode 100644 index 0000000000000..7a6a7ff77335b --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiComboBox } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; + +interface Props { + selectedGroups?: string[]; + onChange: (groupBy: string[]) => void; + fields: IFieldType[]; + label: string; + placeholder: string; +} + +export const GroupBySelector = ({ + onChange, + fields, + selectedGroups = [], + label, + placeholder, +}: Props) => { + const handleChange = useCallback( + (selectedOptions: Array<{ label: string }>) => { + const groupBy = selectedOptions.map((option) => option.label); + onChange(groupBy); + }, + [onChange] + ); + + const formattedSelectedGroups = useMemo(() => { + return selectedGroups.map((group) => ({ label: group })); + }, [selectedGroups]); + + const options = useMemo(() => { + return fields.filter((field) => field.aggregatable).map((field) => ({ label: field.name })); + }, [fields]); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 905b7dfa314bd..018e5098a4291 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -60,6 +60,7 @@ export interface InfraDatabaseSearchResponse skipped: number; failed: number; }; + timed_out: boolean; aggregations?: Aggregations; hits: { total: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index a3b9e85458416..4f1e81e0b2c40 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -55,7 +55,7 @@ services.alertInstanceFactory.mockImplementation((instanceId: string) => { * Helper functions */ function getAlertState(instanceId: string): AlertStates { - const alert = alertInstances.get(instanceId); + const alert = alertInstances.get(`${instanceId}-*`); if (alert) { return alert.state.alertState; } else { @@ -73,11 +73,26 @@ const executor = (createLogThresholdExecutor('test', libsMock) as unknown) as (o // Wrapper to test type Comparison = [number, Comparator, number]; + async function callExecutor( [value, comparator, threshold]: Comparison, criteria: Criterion[] = [] ) { - services.callCluster.mockImplementationOnce(async (..._) => ({ count: value })); + services.callCluster.mockImplementationOnce(async (..._) => ({ + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + timed_out: false, + took: 123456789, + hits: { + total: { + value, + }, + }, + })); return await executor({ services, @@ -90,222 +105,427 @@ async function callExecutor( }); } -describe('Comparators trigger alerts correctly', () => { - it('does not alert when counts do not reach the threshold', async () => { - await callExecutor([0, Comparator.GT, 1]); - expect(getAlertState('test')).toBe(AlertStates.OK); +describe('Ungrouped alerts', () => { + describe('Comparators trigger alerts correctly', () => { + it('does not alert when counts do not reach the threshold', async () => { + await callExecutor([0, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([0, Comparator.GT_OR_EQ, 1]); - expect(getAlertState('test')).toBe(AlertStates.OK); + await callExecutor([0, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([1, Comparator.LT, 0]); - expect(getAlertState('test')).toBe(AlertStates.OK); + await callExecutor([1, Comparator.LT, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([1, Comparator.LT_OR_EQ, 0]); - expect(getAlertState('test')).toBe(AlertStates.OK); - }); + await callExecutor([1, Comparator.LT_OR_EQ, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); + }); - it('alerts when counts reach the threshold', async () => { - await callExecutor([2, Comparator.GT, 1]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + it('alerts when counts reach the threshold', async () => { + await callExecutor([2, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([1, Comparator.GT_OR_EQ, 1]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([1, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([1, Comparator.LT, 2]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([1, Comparator.LT, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([2, Comparator.LT_OR_EQ, 2]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([2, Comparator.LT_OR_EQ, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + }); }); -}); -describe('Comparators create the correct ES queries', () => { - beforeEach(() => { - services.callCluster.mockReset(); - }); + describe('Comparators create the correct ES queries', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); - it('Works with `Comparator.EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ term: { foo: { value: 'bar' } } }], + it('Works with `Comparator.EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + term: { + foo: { + value: 'bar', + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ term: { foo: { value: 'bar' } } }], + it('works with `Comparator.NOT_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + term: { + foo: { + value: 'bar', + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ match: { foo: 'bar' } }], + it('works with `Comparator.MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + match: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ match: { foo: 'bar' } }], + it('works with `Comparator.NOT_MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + match: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ match_phrase: { foo: 'bar' } }], + it('works with `Comparator.MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + match_phrase: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ match_phrase: { foo: 'bar' } }], + it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + match_phrase: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.GT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { gt: 1 } } }], + it('works with `Comparator.GT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + gt: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.GT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { gte: 1 } } }], + it('works with `Comparator.GT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + gte: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.LT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { lt: 1 } } }], + it('works with `Comparator.LT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + lt: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.LT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { lte: 1 } } }], + it('works with `Comparator.LT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + lte: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); }); -}); -describe('Multiple criteria create the right ES query', () => { - beforeEach(() => { - services.callCluster.mockReset(); - }); - it('works', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [ - { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, - { field: 'http.status', comparator: Comparator.LT, value: 400 }, - ] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ term: { foo: { value: 'bar' } } }, { range: { 'http.status': { lt: 400 } } }], + describe('Multiple criteria create the right ES query', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); + it('works', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [ + { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, + { field: 'http.status', comparator: Comparator.LT, value: 400 }, + ] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + term: { + foo: { + value: 'bar', + }, + }, + }, + { + range: { + 'http.status': { + lt: 400, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index ee4e1fcb3f6e2..a2fd01f859385 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -11,10 +11,19 @@ import { Comparator, LogDocumentCountAlertParams, Criterion, + GroupedSearchQueryResponseRT, + UngroupedSearchQueryResponseRT, + UngroupedSearchQueryResponse, + GroupedSearchQueryResponse, + LogDocumentCountAlertParamsRT, } from '../../../../common/alerting/logs/types'; import { InfraBackendLibs } from '../../infra_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InfraSource } from '../../../../common/http_api/source_api'; +import { decodeOrThrow } from '../../../../common/runtime_types'; + +const UNGROUPED_FACTORY_KEY = '*'; +const COMPOSITE_GROUP_SIZE = 40; const checkValueAgainstComparatorMap: { [key: string]: (a: number, b: number) => boolean; @@ -25,37 +34,42 @@ const checkValueAgainstComparatorMap: { [Comparator.LT_OR_EQ]: (a: number, b: number) => a <= b, }; -export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackendLibs) => +export const createLogThresholdExecutor = (alertId: string, libs: InfraBackendLibs) => async function ({ services, params }: AlertExecutorOptions) { - const { count, criteria } = params as LogDocumentCountAlertParams; const { alertInstanceFactory, savedObjectsClient, callCluster } = services; const { sources } = libs; + const { groupBy } = params; const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; - - const alertInstance = alertInstanceFactory(alertUUID); + const alertInstance = alertInstanceFactory(alertId); try { - const query = getESQuery( - params as LogDocumentCountAlertParams, - sourceConfiguration.configuration - ); - const result = await getResults(query, indexPattern, callCluster); - - if (checkValueAgainstComparatorMap[count.comparator](result.count, count.value)) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { - matchingDocuments: result.count, - conditions: createConditionsMessage(criteria), - }); - - alertInstance.replaceState({ - alertState: AlertStates.ALERT, - }); + const validatedParams = decodeOrThrow(LogDocumentCountAlertParamsRT)(params); + + const query = + groupBy && groupBy.length > 0 + ? getGroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern) + : getUngroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern); + + if (!query) { + throw new Error('ES query could not be built from the provided alert params'); + } + + if (groupBy && groupBy.length > 0) { + processGroupByResults( + await getGroupedResults(query, callCluster), + validatedParams, + alertInstanceFactory, + alertId + ); } else { - alertInstance.replaceState({ - alertState: AlertStates.OK, - }); + processUngroupedResults( + await getUngroupedResults(query, callCluster), + validatedParams, + alertInstanceFactory, + alertId + ); } } catch (e) { alertInstance.replaceState({ @@ -66,27 +80,82 @@ export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackend } }; -const getESQuery = ( +const processUngroupedResults = ( + results: UngroupedSearchQueryResponse, params: LogDocumentCountAlertParams, - sourceConfiguration: InfraSource['configuration'] -): object => { + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertId: string +) => { + const { count, criteria } = params; + + const alertInstance = alertInstanceFactory(`${alertId}-${UNGROUPED_FACTORY_KEY}`); + const documentCount = results.hits.total.value; + + if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: null, + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } +}; + +interface ReducedGroupByResults { + name: string; + documentCount: number; +} + +const processGroupByResults = ( + results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], + params: LogDocumentCountAlertParams, + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertId: string +) => { + const { count, criteria } = params; + + const groupResults = results.reduce((acc, groupBucket) => { + const groupName = Object.values(groupBucket.key).join(', '); + const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count }; + return [...acc, groupResult]; + }, []); + + groupResults.forEach((group) => { + const alertInstance = alertInstanceFactory(`${alertId}-${group.name}`); + const documentCount = group.documentCount; + + if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: group.name, + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } + }); +}; + +const buildFiltersFromCriteria = (params: LogDocumentCountAlertParams, timestampField: string) => { const { timeSize, timeUnit, criteria } = params; const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); + const intervalAsMs = intervalAsSeconds * 1000; const to = Date.now(); - const from = to - intervalAsSeconds * 1000; - - const rangeFilters = [ - { - range: { - [sourceConfiguration.fields.timestamp]: { - gte: from, - lte: to, - format: 'epoch_millis', - }, - }, - }, - ]; + const from = to - intervalAsMs; const positiveComparators = getPositiveComparators(); const negativeComparators = getNegativeComparators(); @@ -101,17 +170,121 @@ const getESQuery = ( // Negative assertions (things that "must not" match) const mustNotFilters = buildFiltersForCriteria(negativeCriteria); - const query = { + const rangeFilter = { + range: { + [timestampField]: { + gte: from, + lte: to, + format: 'epoch_millis', + }, + }, + }; + + // For group by scenarios we'll pad the time range by 1 x the interval size on the left (lte) and right (gte), this is so + // a wider net is cast to "capture" the groups. This is to account for scenarios where we want ascertain if + // there were "no documents" (less than 1 for example). In these cases we may be missing documents to build the groups + // and match / not match the criteria. + const groupedRangeFilter = { + range: { + [timestampField]: { + gte: from - intervalAsMs, + lte: to + intervalAsMs, + format: 'epoch_millis', + }, + }, + }; + + return { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters }; +}; + +const getGroupedESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'], + index: string +): object | undefined => { + const { groupBy } = params; + + if (!groupBy || !groupBy.length) { + return; + } + + const timestampField = sourceConfiguration.fields.timestamp; + + const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( + params, + timestampField + ); + + const aggregations = { + groups: { + composite: { + size: COMPOSITE_GROUP_SIZE, + sources: groupBy.map((field, groupIndex) => ({ + [`group-${groupIndex}-${field}`]: { + terms: { field }, + }, + })), + }, + aggregations: { + filtered_results: { + filter: { + bool: { + // Scope the inner filtering back to the unpadded range + filter: [rangeFilter, ...mustFilters], + }, + }, + }, + }, + }, + }; + + const body = { query: { bool: { - filter: [...rangeFilters], - ...(mustFilters.length > 0 && { must: mustFilters }), + filter: [groupedRangeFilter], ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), }, }, + aggregations, + size: 0, }; - return query; + return { + index, + allowNoIndices: true, + ignoreUnavailable: true, + body, + }; +}; + +const getUngroupedESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'], + index: string +): object => { + const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( + params, + sourceConfiguration.fields.timestamp + ); + + const body = { + // Ensure we accurately track the hit count for the ungrouped case, otherwise we can only ensure accuracy up to 10,000. + track_total_hits: true, + query: { + bool: { + filter: [rangeFilter, ...mustFilters], + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, + }, + size: 0, + }; + + return { + index, + allowNoIndices: true, + ignoreUnavailable: true, + body, + }; }; type SupportedESQueryTypes = 'term' | 'match' | 'match_phrase' | 'range'; @@ -145,7 +318,6 @@ const buildCriterionQuery = (criterion: Criterion): Filter | undefined => { }, }, }; - break; case 'match': { return { match: { @@ -221,15 +393,31 @@ const getQueryMappingForComparator = (comparator: Comparator) => { return queryMappings[comparator]; }; -const getResults = async ( - query: object, - index: string, - callCluster: AlertServices['callCluster'] -) => { - return await callCluster('count', { - body: query, - index, - }); +const getUngroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { + return decodeOrThrow(UngroupedSearchQueryResponseRT)(await callCluster('search', query)); +}; + +const getGroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { + let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = []; + let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined; + + while (true) { + const queryWithAfterKey: any = { ...query }; + queryWithAfterKey.body.aggregations.groups.composite.after = lastAfterKey; + const groupResponse: GroupedSearchQueryResponse = decodeOrThrow(GroupedSearchQueryResponseRT)( + await callCluster('search', queryWithAfterKey) + ); + compositeGroupBuckets = [ + ...compositeGroupBuckets, + ...groupResponse.aggregations.groups.buckets, + ]; + lastAfterKey = groupResponse.aggregations.groups.after_key; + if (groupResponse.aggregations.groups.buckets.length < COMPOSITE_GROUP_SIZE) { + break; + } + } + + return compositeGroupBuckets; }; const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index ed7e82fe29e4c..43c298019b632 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -28,6 +28,13 @@ const conditionsActionVariableDescription = i18n.translate( } ); +const groupByActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.groupByActionVariableDescription', + { + defaultMessage: 'The name of the group responsible for triggering the alert', + } +); + const countSchema = schema.object({ value: schema.number(), comparator: schema.oneOf([ @@ -75,6 +82,7 @@ export async function registerLogThresholdAlertType( criteria: schema.arrayOf(criteriaSchema), timeUnit: schema.string(), timeSize: schema.number(), + groupBy: schema.maybe(schema.arrayOf(schema.string())), }), }, defaultActionGroupId: FIRED_ACTIONS.id, @@ -84,6 +92,7 @@ export async function registerLogThresholdAlertType( context: [ { name: 'matchingDocuments', description: documentCountActionVariableDescription }, { name: 'conditions', description: conditionsActionVariableDescription }, + { name: 'group', description: groupByActionVariableDescription }, ], }, producer: 'logs', From 06ee7bd2a3ac28872114db851fa7d6abd7f71e25 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 30 Jun 2020 12:14:21 +0200 Subject: [PATCH 03/19] [ML] Fix license subscription race condition. (#70074) Fixes a race condition where the ML plugin would be mounted before receiving its first license information update and thus redirecting to a fallback page (Kibana Home, Space-Chooser or Data Visualizer page depending on the setup). --- x-pack/plugins/ml/public/application/app.tsx | 6 +- .../application/license/check_license.tsx | 8 ++- .../license/ml_client_license.test.ts | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/license/ml_client_license.test.ts diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 3df176ff25cb4..9539d530bab04 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -80,11 +80,11 @@ export const renderApp = ( deps.kibanaLegacy.loadFontAwesome(); - const mlLicense = setLicenseCache(deps.licensing); - appMountParams.onAppLeave((actions) => actions.default()); - ReactDOM.render(, appMountParams.element); + const mlLicense = setLicenseCache(deps.licensing, [ + () => ReactDOM.render(, appMountParams.element), + ]); return () => { mlLicense.unsubscribe(); diff --git a/x-pack/plugins/ml/public/application/license/check_license.tsx b/x-pack/plugins/ml/public/application/license/check_license.tsx index 3584ee8fbee4b..583eec7d75414 100644 --- a/x-pack/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/plugins/ml/public/application/license/check_license.tsx @@ -5,6 +5,7 @@ */ import { LicensingPluginSetup } from '../../../../licensing/public'; +import { MlLicense } from '../../../common/license'; import { MlClientLicense } from './ml_client_license'; let mlLicense: MlClientLicense | null = null; @@ -16,9 +17,12 @@ let mlLicense: MlClientLicense | null = null; * @param {LicensingPluginSetup} licensingSetup * @returns {MlClientLicense} */ -export function setLicenseCache(licensingSetup: LicensingPluginSetup) { +export function setLicenseCache( + licensingSetup: LicensingPluginSetup, + postInitFunctions?: Array<(lic: MlLicense) => void> +) { mlLicense = new MlClientLicense(); - mlLicense.setup(licensingSetup.license$); + mlLicense.setup(licensingSetup.license$, postInitFunctions); return mlLicense; } diff --git a/x-pack/plugins/ml/public/application/license/ml_client_license.test.ts b/x-pack/plugins/ml/public/application/license/ml_client_license.test.ts new file mode 100644 index 0000000000000..b37d7cfaa00aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/license/ml_client_license.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, Subject } from 'rxjs'; +import { ILicense } from '../../../../licensing/common/types'; + +import { MlClientLicense } from './ml_client_license'; + +describe('MlClientLicense', () => { + test('should miss the license update when initialized without postInitFunction', () => { + const mlLicense = new MlClientLicense(); + + // upon instantiation the full license doesn't get set + expect(mlLicense.isFullLicense()).toBe(false); + + const license$ = new Subject(); + + mlLicense.setup(license$ as Observable); + + // if the observable wasn't triggered the full license is still not set + expect(mlLicense.isFullLicense()).toBe(false); + + license$.next({ + check: () => ({ state: 'valid' }), + getFeature: () => ({ isEnabled: true }), + status: 'valid', + }); + + // once the observable triggered the license should be set + expect(mlLicense.isFullLicense()).toBe(true); + }); + + test('should not miss the license update when initialized with postInitFunction', (done) => { + const mlLicense = new MlClientLicense(); + + // upon instantiation the full license doesn't get set + expect(mlLicense.isFullLicense()).toBe(false); + + const license$ = new Subject(); + + mlLicense.setup(license$ as Observable, [ + (license) => { + // when passed in via postInitFunction callback, the license should be valid + // even if the license$ observable gets triggered after this setup. + expect(license.isFullLicense()).toBe(true); + done(); + }, + ]); + + license$.next({ + check: () => ({ state: 'valid' }), + getFeature: () => ({ isEnabled: true }), + status: 'valid', + }); + }); +}); From 351629f8e9f009e4e134d76c6b730f40d42c0f86 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 30 Jun 2020 13:04:21 +0200 Subject: [PATCH 04/19] updates wording in Cases connectors (#70298) --- .../public/common/lib/connectors/jira/translations.ts | 2 +- .../public/common/lib/connectors/servicenow/translations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts index bcb2c49a0de74..d7abf77a58d4c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const JIRA_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.jira.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new issue in Jira', + defaultMessage: 'Push or update Security case data to a new issue in Jira', } ); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts index 0f06a4259e070..b3e58dcd5b6be 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const SERVICENOW_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.servicenow.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + defaultMessage: 'Push or update Security case data to a new incident in ServiceNow', } ); From 7c352c0702d35f3e68451936dfc7c679dd67e8e1 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 30 Jun 2020 12:38:12 +0100 Subject: [PATCH 05/19] [Dashboard] Add visualization by value to dashboard (#69898) * Plugging in DashboardStart dependency * Create embeddable by reference and navigate back to dashboard * Trying to feature flag the new flow * Feature flagging new visualize flow * Removing unnecessary console statement * Fixing typescript errors * Adding a functional test for new functionality * Adding a functional test for new functionality * Fixing test name * Changing test name * Moving functional test to a separate folder * Trying to fix the config file * Adding an index file * Remove falsly included file * Adding aggs and params to vis input * Serializing vis before passing it as an input * Incorporating new state transfer logic * Remove dashboardStart as a dependency * Trying to get the test to run * Remove unused import * Readding spaces * Fixing type errors * Incorporating new changes --- scripts/functional_tests.js | 1 + .../application/dashboard_app_controller.tsx | 13 +- .../visualize_embeddable_factory.tsx | 21 +- src/plugins/visualize/config.ts | 26 + .../visualize/public/application/types.ts | 2 + .../application/utils/get_top_nav_config.tsx | 22 +- src/plugins/visualize/public/plugin.ts | 5 + src/plugins/visualize/server/index.ts | 11 +- tasks/function_test_groups.js | 2 + .../services/dashboard/visualizations.ts | 26 + test/new_visualize_flow/config.js | 157 ++++++ .../new_visualize_flow/dashboard_embedding.js | 83 +++ .../fixtures/es_archiver/kibana/data.json.gz | Bin 0 -> 20860 bytes .../fixtures/es_archiver/kibana/mappings.json | 490 ++++++++++++++++++ test/new_visualize_flow/index.ts | 27 + 15 files changed, 870 insertions(+), 16 deletions(-) create mode 100644 src/plugins/visualize/config.ts create mode 100644 test/new_visualize_flow/config.js create mode 100644 test/new_visualize_flow/dashboard_embedding.js create mode 100644 test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz create mode 100644 test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json create mode 100644 test/new_visualize_flow/index.ts diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index fc88f2657018f..3fdab481dc750 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,7 @@ const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), require.resolve('../test/plugin_functional/config.js'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), + require.resolve('../test/new_visualize_flow/config.js'), ]; // eslint-disable-next-line no-restricted-syntax const onlyNotInCoverageTests = [ diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index b52bf5bf02b7b..58477d28f9081 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -58,7 +58,6 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, - SavedObjectEmbeddableInput, ContainerOutput, EmbeddableInput, } from '../../../embeddable/public'; @@ -432,14 +431,16 @@ export class DashboardAppController { .getIncomingEmbeddablePackage(); if (incomingState) { if ('id' in incomingState) { - container.addNewEmbeddable(incomingState.type, { + container.addNewEmbeddable(incomingState.type, { savedObjectId: incomingState.id, }); } else if ('input' in incomingState) { - container.addNewEmbeddable( - incomingState.type, - incomingState.input - ); + const input = incomingState.input; + delete input.id; + const explicitInput = { + savedVis: input, + }; + container.addNewEmbeddable(incomingState.type, explicitInput); } } } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index eb4b66401820f..b81ff5c166183 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -30,7 +30,7 @@ import { import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import { Vis } from '../vis'; +import { SerializedVis, Vis } from '../vis'; import { getCapabilities, getTypes, @@ -124,13 +124,20 @@ export class VisualizeEmbeddableFactory } } - public async create() { + public async create(input: VisualizeInput & { savedVis?: SerializedVis }, parent?: IContainer) { // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up // to allow for in place creation of visualizations without having to navigate away to a new URL. - showNewVisModal({ - originatingApp: await this.getCurrentAppId(), - outsideVisualizeApp: true, - }); - return undefined; + if (input.savedVis) { + const visState = input.savedVis; + const vis = new Vis(visState.type, visState); + await vis.setState(visState); + return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + } else { + showNewVisModal({ + originatingApp: await this.getCurrentAppId(), + outsideVisualizeApp: true, + }); + return undefined; + } } } diff --git a/src/plugins/visualize/config.ts b/src/plugins/visualize/config.ts new file mode 100644 index 0000000000000..ee79a37717f26 --- /dev/null +++ b/src/plugins/visualize/config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + showNewVisualizeFlow: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 20d55d1110f62..a6adaf1f3c62b 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -44,6 +44,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { ConfigSchema } from '../../config'; export type PureVisState = SavedVisState; @@ -110,6 +111,7 @@ export interface VisualizeServices extends CoreStart { createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; restorePreviousUrl: () => void; scopedHistory: ScopedHistory; + featureFlagConfig: ConfigSchema; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e04177fc619e2..96f64c6478fa9 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; +import uuid from 'uuid'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../visualizations/public'; import { showSaveModal, @@ -33,7 +34,6 @@ import { unhashUrl } from '../../../../kibana_utils/public'; import { SavedVisInstance, VisualizeServices, VisualizeAppStateContainer } from '../types'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; - interface TopNavConfigParams { hasUnsavedChanges: boolean; setHasUnsavedChanges: (value: boolean) => void; @@ -66,6 +66,7 @@ export const getTopNavConfig = ( toastNotifications, visualizeCapabilities, i18n: { Context: I18nContext }, + featureFlagConfig, }: VisualizeServices ) => { /** @@ -234,6 +235,19 @@ export const getTopNavConfig = ( return response; }; + const createVisReference = () => { + if (!originatingApp) { + return; + } + const input = { + ...vis.serialize(), + id: uuid.v4(), + }; + embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { + state: { input, type: VISUALIZE_EMBEDDABLE_TYPE }, + }); + }; + const saveModal = ( ); - showSaveModal(saveModal, I18nContext); + if (originatingApp === 'dashboards' && featureFlagConfig.showNewVisualizeFlow) { + createVisReference(); + } else { + showSaveModal(saveModal, I18nContext); + } }, }, ] diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 5be560f7fb632..fd9a67599414f 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -60,6 +60,10 @@ export interface VisualizePluginSetupDependencies { data: DataPublicPluginSetup; } +export interface FeatureFlagConfig { + showNewVisualizeFlow: boolean; +} + export class VisualizePlugin implements Plugin { @@ -165,6 +169,7 @@ export class VisualizePlugin savedObjectsPublic: pluginsStart.savedObjects, scopedHistory: params.history, restorePreviousUrl, + featureFlagConfig: this.initializerContext.config.get(), }; params.element.classList.add('visAppWrapper'); diff --git a/src/plugins/visualize/server/index.ts b/src/plugins/visualize/server/index.ts index 5cebef71d8d22..6da0a513b1475 100644 --- a/src/plugins/visualize/server/index.ts +++ b/src/plugins/visualize/server/index.ts @@ -17,8 +17,17 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { VisualizeServerPlugin } from './plugin'; +import { ConfigSchema, configSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + showNewVisualizeFlow: true, + }, + schema: configSchema, +}; + export const plugin = (initContext: PluginInitializerContext) => new VisualizeServerPlugin(initContext); diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 799b9e9eb8194..d60f3ae53eecc 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -41,6 +41,8 @@ const getDefaultArgs = (tag) => { // '--config', 'test/functional/config.firefox.js', '--bail', '--debug', + '--config', + 'test/new_visualize_flow/config.js', ]; }; diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 10747658d8c9b..a5c16010d3eba 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -139,5 +139,31 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F redirectToOrigin: true, }); } + + async createAndEmbedMetric(name: string) { + log.debug(`createAndEmbedMetric(${name})`); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await this.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMetric(); + await find.clickByCssSelector('li.euiListGroupItem:nth-of-type(2)'); + await testSubjects.exists('visualizeSaveButton'); + await testSubjects.click('visualizeSaveButton'); + } + + async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { + log.debug(`createAndEmbedMarkdown(${markdown})`); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await this.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visEditor.setMarkdownTxt(markdown); + await PageObjects.visEditor.clickGo(); + await testSubjects.click('visualizeSaveButton'); + } })(); } diff --git a/test/new_visualize_flow/config.js b/test/new_visualize_flow/config.js new file mode 100644 index 0000000000000..a6440d16481d5 --- /dev/null +++ b/test/new_visualize_flow/config.js @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; + +export default async function ({ readConfigFile }) { + const commonConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + testFiles: [require.resolve('./dashboard_embedding')], + pageObjects, + services, + servers: commonConfig.get('servers'), + + esTestCluster: commonConfig.get('esTestCluster'), + + kbnTestServer: { + ...commonConfig.get('kbnTestServer'), + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + '--oss', + '--telemetry.optIn=false', + '--visualize.showNewVisualizeFlow=true', + ], + }, + + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + 'dateFormat:tz': 'UTC', + }, + }, + + apps: { + kibana: { + pathname: '/app/kibana', + }, + status_page: { + pathname: '/status', + }, + discover: { + pathname: '/app/discover', + hash: '/', + }, + context: { + pathname: '/app/discover', + hash: '/context', + }, + visualize: { + pathname: '/app/visualize', + hash: '/', + }, + dashboard: { + pathname: '/app/dashboards', + hash: '/list', + }, + management: { + pathname: '/app/management', + }, + console: { + pathname: '/app/dev_tools', + hash: '/console', + }, + home: { + pathname: '/app/home', + hash: '/', + }, + }, + junit: { + reportName: 'Chrome UI Functional Tests', + }, + browser: { + type: 'chrome', + }, + + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + //for sample data - can remove but not add sample data + kibana_sample_admin: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['kibana_sample*'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + long_window_logstash: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['long-window-logstash-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + animals: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['animals-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['kibana_admin'], + }, + }; +} diff --git a/test/new_visualize_flow/dashboard_embedding.js b/test/new_visualize_flow/dashboard_embedding.js new file mode 100644 index 0000000000000..b1a6bd14547fb --- /dev/null +++ b/test/new_visualize_flow/dashboard_embedding.js @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +/** + * This tests both that one of each visualization can be added to a dashboard (as opposed to opening an existing + * dashboard with the visualizations already on it), as well as conducts a rough type of snapshot testing by checking + * for various ui components. The downside is these tests are a bit fragile to css changes (though not as fragile as + * actual screenshot snapshot regression testing), and can be difficult to diagnose failures (which visualization + * broke?). The upside is that this offers very good coverage with a minimal time investment. + */ + +export default function ({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dashboardExpect = getService('dashboardExpect'); + const testSubjects = getService('testSubjects'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'visualize', + 'discover', + 'timePicker', + ]); + + describe('Dashboard Embedding', function describeIndexTests() { + before(async () => { + await esArchiver.load('kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('adding a metric visualization', async function () { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(originalPanelCount).to.eql(0); + await testSubjects.exists('addVisualizationButton'); + await testSubjects.click('addVisualizationButton'); + await dashboardVisualizations.createAndEmbedMetric('Embedding Vis Test'); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['0']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + + it('adding a markdown', async function () { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(originalPanelCount).to.eql(1); + await testSubjects.exists('dashboardAddNewPanelButton'); + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.createAndEmbedMarkdown({ + name: 'Embedding Markdown Test', + markdown: 'Nice to meet you, markdown is my name', + }); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.markdownWithValuesExists(['Nice to meet you, markdown is my name']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + }); +} diff --git a/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz b/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ae78761fef0d3415c8ec05ea4bfa9dcca070981a GIT binary patch literal 20860 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 literal 0 HcmV?d00001 diff --git a/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json b/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json new file mode 100644 index 0000000000000..9f5edaad0fe76 --- /dev/null +++ b/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json @@ -0,0 +1,490 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "notifications:lifetime:banner": { + "type": "long" + }, + "notifications:lifetime:error": { + "type": "long" + }, + "notifications:lifetime:info": { + "type": "long" + }, + "notifications:lifetime:warning": { + "type": "long" + }, + "xPackMonitoring:showBanner": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/test/new_visualize_flow/index.ts b/test/new_visualize_flow/index.ts new file mode 100644 index 0000000000000..e915525155990 --- /dev/null +++ b/test/new_visualize_flow/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { FtrProviderContext } from '../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: FtrProviderContext) { + describe('New Visualize Flow', function () { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./dashboard_embedding')); + }); +} From 6027451687ed5697cab48bc3dff30ca391193fa4 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 30 Jun 2020 15:27:39 +0200 Subject: [PATCH 06/19] [code coverage] ingest correct coveredFilePath for mocha (#70215) * [code coverage] ingest correct coveredFilePath for mocha * export variable globally so it is availble in node script --- src/dev/code_coverage/shell_scripts/ingest_coverage.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index b7064a1e42671..2dae75484d68f 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -17,7 +17,7 @@ export ES_HOST STATIC_SITE_URL_BASE='https://kibana-coverage.elastic.dev' export STATIC_SITE_URL_BASE -for x in jest functional mocha; do +for x in jest functional; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json @@ -25,5 +25,11 @@ for x in jest functional mocha; do node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt done +# Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path +COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json +export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana + +node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt + echo "### Ingesting Code Coverage - Complete" echo "" From 82fd107fc2df3e38c4c9d5f906826cba3f4de468 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 30 Jun 2020 15:55:15 +0200 Subject: [PATCH 07/19] Fix typo in bootstrap command (#69976) --- packages/kbn-pm/dist/index.js | 2 +- packages/kbn-pm/src/commands/bootstrap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 69611ed3f5c5e..b8794124ad197 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -8868,7 +8868,7 @@ const BootstrapCommand = { } if (cachedProjectCount > 0) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`${cachedProjectCount} bootsrap builds are cached`); + _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`${cachedProjectCount} bootstrap builds are cached`); } await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_2__["parallelizeBatches"])(batchedProjects, async project => { diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index f8e50a8247856..a559f9a20432a 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -82,7 +82,7 @@ export const BootstrapCommand: ICommand = { } if (cachedProjectCount > 0) { - log.success(`${cachedProjectCount} bootsrap builds are cached`); + log.success(`${cachedProjectCount} bootstrap builds are cached`); } await parallelizeBatches(batchedProjects, async (project) => { From 3caab366c7000a498d9e26c9c5dad2f248314ff8 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 30 Jun 2020 08:04:48 -0600 Subject: [PATCH 08/19] [Maps] Add maps telemetry saved object in with mappings disabled (#69995) Co-authored-by: Rudolf Meijering Co-authored-by: Elastic Machine --- x-pack/plugins/maps/server/plugin.ts | 3 ++- .../maps/server/saved_objects/index.ts | 1 + .../server/saved_objects/maps_telemetry.ts | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 60f3a9b68202c..dbcce50ac2b9a 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -15,7 +15,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; -import { mapSavedObjects } from './saved_objects'; +import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore import { setInternalRepository } from './kibana_server_services'; @@ -191,6 +191,7 @@ export class MapsPlugin implements Plugin { }, }); + core.savedObjects.registerType(mapsTelemetrySavedObjects); core.savedObjects.registerType(mapSavedObjects); registerMapsUsageCollector(usageCollection, currentConfig); diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index 804d720a13ab0..c4b779183a2de 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,4 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +export { mapsTelemetrySavedObjects } from './maps_telemetry'; export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts new file mode 100644 index 0000000000000..c0d36983f65cd --- /dev/null +++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'src/core/server'; + +/* + * The maps-telemetry saved object type isn't used, but in order to remove these fields from + * the mappings we register this type with `type: 'object', enabled: true` to remove all + * previous fields from the mappings until https://github.com/elastic/kibana/issues/67086 is + * solved. + */ +export const mapsTelemetrySavedObjects: SavedObjectsType = { + name: 'maps-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + // @ts-ignore Core types don't support this since it's only really valid when removing a previously registered type + type: 'object', + enabled: false, + }, +}; From 233d261674799668d0938ee033c8632e377cebc0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 30 Jun 2020 10:07:50 -0400 Subject: [PATCH 09/19] [ML] Anomaly Detection: ensure 'Category examples' tab in the expanded table row can be seen (#70241) * remove space from tab id * update test --- .../application/components/anomalies_table/anomaly_details.js | 2 +- .../components/anomalies_table/anomaly_details.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index edc1790b3adac..7b979d74a329c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -279,7 +279,7 @@ export class AnomalyDetails extends Component { ), }, { - id: 'Category examples', + id: 'category-examples', name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.categoryExamplesTitle', { defaultMessage: 'Category examples', }), diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js index 9fd1ffc3b637f..78c036eac1903 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js @@ -67,7 +67,7 @@ describe('AnomalyDetails', () => { tabIndex: 1, }; const wrapper = shallowWithIntl(); - expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples'); + expect(wrapper.prop('initialSelectedTab').id).toBe('category-examples'); }); test('Renders with terms and regex when definition prop is not undefined', () => { From ad01223c5acba50ee71771a2330ac95a370c651e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 30 Jun 2020 16:13:28 +0200 Subject: [PATCH 10/19] chore: improve support for mjs file extension (#70186) --- .eslintrc.js | 56 +++++++++---------- .../core/development-unit-tests.asciidoc | 2 +- packages/eslint-config-kibana/jest.js | 4 +- src/dev/jest/config.js | 4 +- src/dev/run_eslint.js | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 12 ++-- x-pack/plugins/apm/jest.config.js | 6 +- x-pack/test_utils/jest/config.integration.js | 4 +- x-pack/test_utils/jest/config.js | 4 +- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 32f59c4d6b3db..2c49bf78c67b1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,63 +64,63 @@ module.exports = { * Temporarily disable some react rules for specific plugins, remove in separate PRs */ { - files: ['packages/kbn-ui-framework/**/*.{js,ts,tsx}'], + files: ['packages/kbn-ui-framework/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/no-onchange': 'off', }, }, { - files: ['src/plugins/es_ui_shared/**/*.{js,ts,tsx}'], + files: ['src/plugins/es_ui_shared/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['src/plugins/kibana_react/**/*.{js,ts,tsx}'], + files: ['src/plugins/kibana_react/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/rules-of-hooks': 'off', 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['src/plugins/kibana_utils/**/*.{js,ts,tsx}'], + files: ['src/plugins/kibana_utils/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/plugins/canvas/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/canvas/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, }, { - files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, }, { - files: ['x-pack/legacy/plugins/index_management/**/*.{js,ts,tsx}'], + files: ['x-pack/legacy/plugins/index_management/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', 'react-hooks/rules-of-hooks': 'off', }, }, { - files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/plugins/ml/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/ml/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,ts,tsx}'], + files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, @@ -132,7 +132,7 @@ module.exports = { * Licence headers */ { - files: ['**/*.{js,ts,tsx}', '!plugins/**/*'], + files: ['**/*.{js,mjs,ts,tsx}', '!plugins/**/*'], rules: { '@kbn/eslint/require-license-header': [ 'error', @@ -153,7 +153,7 @@ module.exports = { * New Platform client-side */ { - files: ['{src,x-pack}/plugins/*/public/**/*.{js,ts,tsx}'], + files: ['{src,x-pack}/plugins/*/public/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-commonjs': 'error', }, @@ -163,7 +163,7 @@ module.exports = { * Files that require Elastic license headers instead of Apache 2.0 header */ { - files: ['x-pack/**/*.{js,ts,tsx}'], + files: ['x-pack/**/*.{js,mjs,ts,tsx}'], rules: { '@kbn/eslint/require-license-header': [ 'error', @@ -184,7 +184,7 @@ module.exports = { * Restricted paths */ { - files: ['**/*.{js,ts,tsx}'], + files: ['**/*.{js,mjs,ts,tsx}'], rules: { '@kbn/eslint/no-restricted-paths': [ 'error', @@ -251,8 +251,8 @@ module.exports = { ], from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', - '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,ts}', - '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', + '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,mjs,ts}', + '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,mjs,ts,tsx}', ], allowSameFolder: true, errorMessage: 'Plugins may only import from top-level public and server modules.', @@ -264,11 +264,11 @@ module.exports = { 'src/legacy/core_plugins/**/*', '!src/legacy/core_plugins/**/server/**/*', - '!src/legacy/core_plugins/**/index.{js,ts,tsx}', + '!src/legacy/core_plugins/**/index.{js,mjs,ts,tsx}', 'x-pack/legacy/plugins/**/*', '!x-pack/legacy/plugins/**/server/**/*', - '!x-pack/legacy/plugins/**/index.{js,ts,tsx}', + '!x-pack/legacy/plugins/**/index.{js,mjs,ts,tsx}', 'examples/**/*', '!examples/**/server/**/*', @@ -530,7 +530,7 @@ module.exports = { * Jest specific rules */ { - files: ['**/*.test.{js,ts,tsx}'], + files: ['**/*.test.{js,mjs,ts,tsx}'], rules: { 'jest/valid-describe': 'error', }, @@ -595,8 +595,8 @@ module.exports = { { // front end and common typescript and javascript files only files: [ - 'x-pack/plugins/security_solution/public/**/*.{js,ts,tsx}', - 'x-pack/plugins/security_solution/common/**/*.{js,ts,tsx}', + 'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -646,7 +646,7 @@ module.exports = { // { // // will introduced after the other warns are fixed // // typescript and javascript for front end react performance - // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,ts,tsx}'], + // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,mjs,ts,tsx}'], // plugins: ['react-perf'], // rules: { // // 'react-perf/jsx-no-new-object-as-prop': 'error', @@ -657,7 +657,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, @@ -776,8 +776,8 @@ module.exports = { { // front end and common typescript and javascript files only files: [ - 'x-pack/plugins/lists/public/**/*.{js,ts,tsx}', - 'x-pack/plugins/lists/common/**/*.{js,ts,tsx}', + 'x-pack/plugins/lists/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/lists/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -792,7 +792,7 @@ module.exports = { }, { // typescript and javascript for front and back end - files: ['x-pack/plugins/lists/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lists/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node'], env: { mocha: true, @@ -1020,8 +1020,8 @@ module.exports = { */ { files: [ - 'src/plugins/vis_type_timeseries/**/*.{js,ts,tsx}', - 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,ts,tsx}', + 'src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', + 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-default-export': 'error', diff --git a/docs/developer/core/development-unit-tests.asciidoc b/docs/developer/core/development-unit-tests.asciidoc index a738e2cf372d9..04cce0dfec901 100644 --- a/docs/developer/core/development-unit-tests.asciidoc +++ b/docs/developer/core/development-unit-tests.asciidoc @@ -22,7 +22,7 @@ yarn test:mocha [float] ==== Jest -Jest tests are stored in the same directory as source code files with the `.test.{js,ts,tsx}` suffix. +Jest tests are stored in the same directory as source code files with the `.test.{js,mjs,ts,tsx}` suffix. *Running Jest Unit Tests* diff --git a/packages/eslint-config-kibana/jest.js b/packages/eslint-config-kibana/jest.js index d682277ff905a..c374de7ae123c 100644 --- a/packages/eslint-config-kibana/jest.js +++ b/packages/eslint-config-kibana/jest.js @@ -2,8 +2,8 @@ module.exports = { overrides: [ { files: [ - '**/*.{test,test.mocks,mock}.{js,ts,tsx}', - '**/__mocks__/**/*.{js,ts,tsx}', + '**/*.{test,test.mocks,mock}.{js,mjs,ts,tsx}', + '**/__mocks__/**/*.{js,mjs,ts,tsx}', ], plugins: [ 'jest', diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index da343aa0f0672..391a52b7f0397 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -50,7 +50,7 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,jsx,ts,tsx}', + 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', '!src/legacy/core_plugins/tests_bundle/**', ], @@ -81,7 +81,7 @@ export default { ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', testMatch: ['**/*.test.{js,ts,tsx}'], diff --git a/src/dev/run_eslint.js b/src/dev/run_eslint.js index 3bfbb9cc876e0..3214a2fb45471 100644 --- a/src/dev/run_eslint.js +++ b/src/dev/run_eslint.js @@ -31,7 +31,7 @@ if (!process.argv.includes('--no-cache')) { } if (!process.argv.includes('--ext')) { - process.argv.push('--ext', '.js,.ts,.tsx'); + process.argv.push('--ext', '.js,.mjs,.ts,.tsx'); } // common-js is required so that logic before this executes before loading eslint diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 9b6db8b74458b..a0574dbdf36da 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -9,7 +9,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector return { rootDir, roots: ['/plugins', '/legacy/plugins', '/legacy/server'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, @@ -32,11 +32,11 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^(!!)?file-loader!': fileMockPath, }, collectCoverageFrom: [ - 'legacy/plugins/**/*.{js,jsx,ts,tsx}', - 'legacy/server/**/*.{js,jsx,ts,tsx}', - 'plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/plugins/**/*.{js,mjs,jsx,ts,tsx}', + 'legacy/server/**/*.{js,mjs,jsx,ts,tsx}', + 'plugins/**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', - '!**/*.test.{js,ts,tsx}', + '!**/*.test.{js,mjs,ts,tsx}', '!**/flot-charts/**', '!**/test/**', '!**/build/**', @@ -60,7 +60,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, ], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testRunner: 'jest-circus/runner', transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 43bdeb583c819..2f9d8a37376d9 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,10 +29,10 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ - '**/*.{js,jsx,ts,tsx}', + '**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', - '!**/*.stories.{js,ts,tsx}', - '!**/*.test.{js,ts,tsx}', + '!**/*.stories.{js,mjs,ts,tsx}', + '!**/*.test.{js,mjs,ts,tsx}', '!**/dev_docs/**', '!**/e2e/**', '!**/scripts/**', diff --git a/x-pack/test_utils/jest/config.integration.js b/x-pack/test_utils/jest/config.integration.js index 033c948c3c034..03917d34ab09c 100644 --- a/x-pack/test_utils/jest/config.integration.js +++ b/x-pack/test_utils/jest/config.integration.js @@ -10,9 +10,9 @@ import config from './config'; export default { ...config, testMatch: [ - `**/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,ts,tsx}`, + `**/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,mjs,ts,tsx}`, // Tests within `__jest__` directories should be treated as regular unit tests. - `!**/__jest__/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,ts,tsx}`, + `!**/__jest__/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,mjs,ts,tsx}`, ], testPathIgnorePatterns: config.testPathIgnorePatterns.filter( (pattern) => !pattern.includes(RESERVED_DIR_JEST_INTEGRATION_TESTS) diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index adee510ef2846..7bb073023b7f8 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -29,10 +29,10 @@ export default { ], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: ['html'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ '/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/', '/packages/kbn-pm/dist/', From 93ef5c0c418374fdd145a3ed321fa6700a8132e3 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 30 Jun 2020 07:30:31 -0700 Subject: [PATCH 11/19] [Usage Collection] Report nodes feature usage (#70108) * Adds nodes feature usage stats merged into cluster_stats.nodes when usage collection is local --- .../__tests__/get_local_stats.js | 65 +++++++++++++-- .../telemetry_collection/get_local_stats.ts | 14 +++- .../get_nodes_usage.test.ts | 80 ++++++++++++++++++ .../telemetry_collection/get_nodes_usage.ts | 81 +++++++++++++++++++ .../apis/telemetry/telemetry_local.js | 1 + .../get_stats_with_xpack.test.ts.snap | 44 +++++++++- .../get_stats_with_xpack.test.ts | 25 ++++++ 7 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index 29076537e9ae8..e78b92498e6e7 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -19,11 +19,12 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; +import { merge, omit } from 'lodash'; +import { TIMEOUT } from '../constants'; import { mockGetClusterInfo } from './get_cluster_info'; import { mockGetClusterStats } from './get_cluster_stats'; -import { omit } from 'lodash'; import { getLocalStats, handleLocalStats } from '../get_local_stats'; const mockUsageCollection = (kibanaUsage = {}) => ({ @@ -51,10 +52,26 @@ const getMockServer = (getCluster = sinon.stub()) => ({ elasticsearch: { getCluster }, }, }); +function mockGetNodesUsage(callCluster, nodesUsage, req) { + callCluster + .withArgs( + req, + { + method: 'GET', + path: '/_nodes/usage', + query: { + timeout: TIMEOUT, + }, + }, + 'transport.request' + ) + .returns(nodesUsage); +} -function mockGetLocalStats(callCluster, clusterInfo, clusterStats, req) { +function mockGetLocalStats(callCluster, clusterInfo, clusterStats, nodesUsage, req) { mockGetClusterInfo(callCluster, clusterInfo, req); mockGetClusterStats(callCluster, clusterStats, req); + mockGetNodesUsage(callCluster, nodesUsage, req); } describe('get_local_stats', () => { @@ -68,6 +85,28 @@ describe('get_local_stats', () => { number: version, }, }; + const nodesUsage = [ + { + node_id: 'some_node_id', + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + ]; const clusterStats = { _nodes: { failed: 123 }, cluster_name: 'real-cool', @@ -75,6 +114,7 @@ describe('get_local_stats', () => { nodes: { yup: 'abc' }, random: 123, }; + const kibana = { kibana: { great: 'googlymoogly', @@ -97,12 +137,16 @@ describe('get_local_stats', () => { snow: { chances: 0 }, }; + const clusterStatsWithNodesUsage = { + ...clusterStats, + nodes: merge(clusterStats.nodes, { usage: nodesUsage }), + }; const combinedStatsResult = { collection: 'local', cluster_uuid: clusterUuid, cluster_name: clusterName, version, - cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), + cluster_stats: omit(clusterStatsWithNodesUsage, '_nodes', 'cluster_name'), stack_stats: { kibana: { great: 'googlymoogly', @@ -135,7 +179,7 @@ describe('get_local_stats', () => { describe('handleLocalStats', () => { it('returns expected object without xpack and kibana data', () => { - const result = handleLocalStats(clusterInfo, clusterStats, void 0, context); + const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); @@ -146,7 +190,7 @@ describe('get_local_stats', () => { }); it('returns expected object with xpack', () => { - const result = handleLocalStats(clusterInfo, clusterStats, void 0, context); + const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); @@ -167,7 +211,8 @@ describe('get_local_stats', () => { mockGetLocalStats( callClusterUsageFailed, Promise.resolve(clusterInfo), - Promise.resolve(clusterStats) + Promise.resolve(clusterStats), + Promise.resolve(nodesUsage) ); const result = await getLocalStats([], { server: getMockServer(), @@ -177,6 +222,7 @@ describe('get_local_stats', () => { expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); + expect(result.cluster_stats.nodes).to.eql(combinedStatsResult.cluster_stats.nodes); expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); @@ -188,7 +234,12 @@ describe('get_local_stats', () => { it('returns expected object with xpack and kibana data', async () => { const callCluster = sinon.stub(); const usageCollection = mockUsageCollection(kibana); - mockGetLocalStats(callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats)); + mockGetLocalStats( + callCluster, + Promise.resolve(clusterInfo), + Promise.resolve(clusterStats), + Promise.resolve(nodesUsage) + ); const result = await getLocalStats([], { server: getMockServer(callCluster), diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index b77d01c5b431f..b42edde2f55ca 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -24,6 +24,7 @@ import { import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; +import { getNodesUsage } from './get_nodes_usage'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -67,12 +68,21 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( return await Promise.all( clustersDetails.map(async (clustersDetail) => { - const [clusterInfo, clusterStats, kibana] = await Promise.all([ + const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) + getNodesUsage(callCluster), // nodes_usage info getKibana(usageCollection, callCluster), ]); - return handleLocalStats(clusterInfo, clusterStats, kibana, context); + return handleLocalStats( + clusterInfo, + { + ...clusterStats, + nodes: { ...clusterStats.nodes, usage: nodesUsage }, + }, + kibana, + context + ); }) ); }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts new file mode 100644 index 0000000000000..4e4b0e11b7979 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getNodesUsage } from './get_nodes_usage'; +import { TIMEOUT } from './constants'; + +const mockedNodesFetchResponse = { + cluster_name: 'test cluster', + nodes: { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + }, +}; +describe('get_nodes_usage', () => { + it('calls fetchNodesUsage', async () => { + const callCluster = jest.fn(); + callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); + await getNodesUsage(callCluster); + expect(callCluster).toHaveBeenCalledWith('transport.request', { + path: '/_nodes/usage', + method: 'GET', + query: { + timeout: TIMEOUT, + }, + }); + }); + it('returns a modified array of node usage data', async () => { + const callCluster = jest.fn(); + callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); + const result = await getNodesUsage(callCluster); + expect(result.nodes).toEqual([ + { + aggregations: { scripted_metric: { other: 7 }, terms: { bytes: 2 } }, + node_id: 'some_node_id', + rest_actions: { + create_index_action: 1, + document_get_action: 1, + nodes_info_action: 36, + nodes_usage_action: 1, + search_action: 19, + }, + since: 1588616945163, + timestamp: 1588617023177, + }, + ]); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts new file mode 100644 index 0000000000000..c5c110fbb4149 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { LegacyAPICaller } from 'kibana/server'; +import { TIMEOUT } from './constants'; + +export interface NodeAggregation { + [key: string]: number; +} + +// we set aggregations as an optional type because it was only added in v7.8.0 +export interface NodeObj { + node_id?: string; + timestamp: number; + since: number; + rest_actions: { + [key: string]: number; + }; + aggregations?: { + [key: string]: NodeAggregation; + }; +} + +export interface NodesFeatureUsageResponse { + cluster_name: string; + nodes: { + [key: string]: NodeObj; + }; +} + +export type NodesUsageGetter = ( + callCluster: LegacyAPICaller +) => Promise<{ nodes: NodeObj[] | Array<{}> }>; +/** + * Get the nodes usage data from the connected cluster. + * + * This is the equivalent to GET /_nodes/usage?timeout=30s. + * + * The Nodes usage API was introduced in v6.0.0 + */ +export async function fetchNodesUsage( + callCluster: LegacyAPICaller +): Promise { + const response = await callCluster('transport.request', { + method: 'GET', + path: '/_nodes/usage', + query: { + timeout: TIMEOUT, + }, + }); + return response; +} + +/** + * Get the nodes usage from the connected cluster + * @param callCluster APICaller + * @returns Object containing array of modified usage information with the node_id nested within the data for that node. + */ +export const getNodesUsage: NodesUsageGetter = async (callCluster) => { + const result = await fetchNodesUsage(callCluster); + const transformedNodes = Object.entries(result?.nodes || {}).map(([key, value]) => ({ + ...(value as NodeObj), + node_id: key, + })); + return { nodes: transformedNodes }; +}; diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index 2875ff09a9a8d..e74cd180185ab 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -113,6 +113,7 @@ export default function ({ getService }) { 'cluster_stats.nodes.plugins', 'cluster_stats.nodes.process', 'cluster_stats.nodes.versions', + 'cluster_stats.nodes.usage', 'cluster_stats.status', 'cluster_stats.timestamp', 'cluster_uuid', diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index 1a70504dc9391..ed82dc65eb410 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -4,7 +4,27 @@ exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no licen Array [ Object { "cluster_name": "test", - "cluster_stats": Object {}, + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, + }, + }, + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, + }, + "since": 1588616945163, + "timestamp": 1588617023177, + }, + ], + }, + }, + }, "cluster_uuid": "test", "collection": "local", "stack_stats": Object { @@ -62,7 +82,27 @@ exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + Array [ Object { "cluster_name": "test", - "cluster_stats": Object {}, + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, + }, + }, + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, + }, + "since": 1588616945163, + "timestamp": 1588617023177, + }, + ], + }, + }, + }, "cluster_uuid": "test", "collection": "local", "stack_stats": Object { diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 5dfe3d3e99a7f..a8311933f0531 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -28,6 +28,20 @@ const kibana = { rain: { chances: 2 }, snow: { chances: 0 }, }; +const nodesUsage = { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + }, + aggregations: { + terms: { + bytes: 2, + }, + }, + }, +}; const getContext = () => ({ version: '8675309-snapshot', @@ -47,6 +61,11 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { if (options.path === '/_license' || options.path === '/_xpack/usage') { // eslint-disable-next-line no-throw-literal throw { statusCode: 404 }; + } else if (options.path === '/_nodes/usage') { + return { + cluster_name: 'test cluster', + nodes: nodesUsage, + }; } return {}; case 'info': @@ -81,6 +100,12 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { if (options.path === '/_xpack/usage') { return {}; } + if (options.path === '/_nodes/usage') { + return { + cluster_name: 'test cluster', + nodes: nodesUsage, + }; + } case 'info': return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; default: From 8978ec8945df57b5df76a3b404b1bf35e0762c91 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 30 Jun 2020 07:31:47 -0700 Subject: [PATCH 12/19] [DOCS] Adds glossary to documentation (#69721) * [DOCS] Adds glossary to documentation * [DOCS] Fixes build errors * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * [DOCS] Adds more terms to glossary * [DOCS] Adds more terms to glossary * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * [DOCS] Incorporates review comments * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * [DOCS] Incorporates review comments * [DOCS] Incorporates review comments Co-authored-by: Lisa Cawley Co-authored-by: debadair --- docs/glossary.asciidoc | 413 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/glossary.asciidoc diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc new file mode 100644 index 0000000000000..d7a82068abbcb --- /dev/null +++ b/docs/glossary.asciidoc @@ -0,0 +1,413 @@ +[glossary] +[[glossary]] += Glossary + +<> | <> | <> | <> | <> | <> | <> | H | I | J | <> | <> | <> | N | O | <> | <> | R | <> | <> | <> | V | <> | X | Y | Z + +[float] +[[a_glos]] +== A + +[glossary] +[[glossary-action]] action :: ++ +-- +// tag::action-def[] +The alert-specific response that occurs when an alert fires. +An alert can have multiple actions. +See +{kibana-ref}/action-types.html[Action and connector types]. +// end::action-def[] +-- + +[[glossary-advanced-settings]] Advanced Settings :: +// tag::advanced-settings-def[] +Enables you to control the appearance and behavior of {kib} +by setting the date format, default index, and other attributes. +Part of {kib} Stack Management. +See {kibana-ref}/advanced-options.html[Advanced Settings]. +// end::advanced-settings-def[] + +[[glossary-alert]] alert :: +// tag::alert-def[] +A set of <>, schedules, and <> +that enable notifications. +See <>. +// end::alert-def[] + +[[glossary-alerts-and-actions]] Alerts and Actions :: +// tag::alerts-and-actions-def[] +A comprehensive view of all your alerts. Enables you to access and +manage alerts for all {kib} apps from one place. +See {kibana-ref}/alerting-getting-started.html[Alerts and Actions]. +// end::alerts-and-actions-def[] + +[[glossary-annotation]] annotation :: +// tag::annotation-def[] +A way to augment a data display with descriptive domain knowledge. +// end::alerts-annotation-def[] + + +[[glossary-app]] app :: +// tag::app-def[] +A top-level {kib} component that is accessed through the side navigation. +Apps include core {kib} components such as Discover and Dashboard, +solutions like Observability and Security, and special-purpose tools +like Maps and Stack Management. +// end::app-def[] + + +[float] +[[b_glos]] +== B + +[[glossary-basemap]] basemap :: +// tag::basemap-def[] +The background detail necessary to orient the location of a map. +// end::basemap-def[] + +[[glossary-bucket]] bucket :: +// tag::bucket-def[] +A set of documents in {kib} that have certain characteristics in common. +For example, matching documents might be bucketed by color, distance, or date range. +// end::bucket-def[] + +[[glossary-bucket-aggregation]] bucket aggregation:: +// tag::bucket-aggregation-def[] +An aggregation that creates buckets of documents. Each bucket is associated with a +criterion (depending on the aggregation type), which determines whether or not a document +in the current context falls into the bucket. +// end::bucket-aggregation-def[] + +[float] +[[c_glos]] +== C + +[[glossary-canvas]] Canvas :: +// tag::canvas-def[] +Enables you to create presentations and infographics that pull live data directly from {es}. +See {kibana-ref}/canvas.html[Canvas]. +// end::canvas-def[] + +[[glossary-canvas-language]] Canvas expression language:: +// tag::ccanvas-language-def[] +A pipeline-based expression language for manipulating and visualizing data. +Includes dozens of functions and other capabilities, such as table transforms, +type casting, and sub-expressions. Supports TinyMath functions for complex math calculations. +See {kibana-ref}/canvas-function-reference.html[Canvas function reference]. +// end::canvas-language-def[] + + +[[glossary-certainty]] certainty :: +// tag::certainty-def[] +Specifies how many documents must contain a pair of terms before it is considered +a useful connection in a graph. +// end::certainty-def[] + +[[glossary-condition]] condition :: +// tag::condition-def[] +Specifies the circumstances that must be met to trigger an alert. +// end::condition-def[] + +[[glossary-connector]] connector :: +// tag::connector-def[] +A configuration that enables integration with an external system (the destination for an action). +See {kibana-ref}/action-types.html[Action and connector types]. +// end::connector-def[] + +[[glossary-console]] Console :: +// tag::console-def[] +A tool for interacting with the {es} REST API. +You can send requests to {es}, view responses, +view API documentation, and get your request history. +See {kibana-ref}/console-kibana.html[Console]. +// end::console-def[] + +[float] +[[d_glos]] +== D + +[[glossary-dashboard]] dashboard :: +// tag::dashboard-def[] +A collection of +<>, <>, and +<> that +provide insights into your data from multiple perspectives. +// end::dashboard-def[] + +[[glossary-data-source]] data source :: +// tag::data-source-def[] +A file, database, or service that provides the underlying data for a map, Canvas element, or visualization. +// end::data-source-def[] + +[[glossary-discover]] Discover :: +// tag::discover-def[] +Enables you to search and filter your data to zoom in on the information +that you are interested in. +// end::discover-def[] + +[[glossary-drilldown]] drilldown :: +// tag::drilldown-def[] +A navigation path that retains context (time range and filters) +from the source to the destination, so you can view the data from a new perspective. +A dashboard that shows the overall status of multiple data centers +might have a drilldown to a dashboard for a single data center. See {kibana-ref}/drilldowns.html[Drilldowns]. +// end::drilldown-def[] + + + +[float] +[[e_glos]] +== E + +[[glossary-edge]] edge :: +// tag::edge-def[] +A connection between nodes in a graph that shows that they are related. +The line weight indicates the strength of the relationship. See +{kibana-ref}/xpack-graph.html[Graph]. +// end::edge-def[] + + +[[glossary-ems]] Elastic Maps Service (EMS) :: +// tag::ems-def[] +A service that provides basemap tiles, shape files, and other key features +that are essential for visualizing geospatial data. +// end::ems-def[] + +[[glossary-element]] element :: +// tag::element-def[] +A <> workpad object that displays an image, text, or visualization. +// end::element-def[] + + +[float] +[[f_glos]] +== F + +[[glossary-feature-controls]] Feature Controls :: +// tag::feature-controls-def[] +Enables administrators to customize which features are +available in each <>. See +{kibana-ref}//xpack-spaces.html#spaces-control-feature-visibility[Feature Controls]. +// end::feature-controls-def[] + +[float] +[[g_glos]] +== G + +[[glossary-graph]] graph :: +// tag::graph-def[] +A data structure and visualization that shows interconnections between +a set of entities. Each entity is represented by a node. Connections between +nodes are represented by <>. See {kibana-ref}/xpack-graph.html[Graph]. +// end::graph-def[] + +[[glossary-grok-debugger]] Grok Debugger :: +// tag::grok-debugger-def[] +A tool for building and debugging grok patterns. Grok is good for parsing +syslog, Apache, and other webserver logs. See +{kibana-ref}/xpack-grokdebugger.html[Debugging grok expressions]. +// end::grok-debugger-def[] + + +[float] +[[k_glos]] +== K + +[[glossary-kql]] {kib} Query Language (KQL) :: +// tag::kql-def[] +The default language for querying in {kib}. KQL provides +support for scripted fields. See +{kibana-ref}/kuery-query.html[Kibana Query Language]. +// end::kql-def[] + + +[float] +[[l_glos]] +== L + +[[glossary-lens]] Lens :: +// tag::lens-def[] +Enables you to build visualizations by dragging and dropping data fields. +Lens makes makes smart visualization suggestions for your data, +allowing you to switch between visualization types. +See {kibana-ref}/lens.html[Lens]. +// end::lens-def[] + + +[[glossary-lucene]] Lucene query syntax :: +// tag::lucene-def[] +The query syntax for {kib}’s legacy query language. The Lucene query +syntax is available under the options menu in the query bar and from +<>. +// end::lucene-def[] + +[float] +[[m_glos]] +== M + +[[glossary-map]] map :: +// tag::map-def[] +A representation of geographic data using symbols and labels. +See {kibana-ref}/maps.html[Maps]. +// end::map-def[] + +[[glossary-metric-aggregation]] metric aggregation :: +// tag::metric-aggregation-def[] +An aggregation that calculates and tracks metrics for a set of documents. +// end::metric-aggregation-def[] + + +[float] +[[p_glos]] +== P + +[[glossary-painless-lab]] Painless Lab :: +// tag::painless-lab-def[] +An interactive code editor that lets you test and debug Painless scripts in real-time. +See {kibana-ref}/painlesslab.html[Painless Lab]. +// end::painless-lab-def[] + + +[[glossary-panel]] panel :: +// tag::panel-def[] +A <> component that contains a +query element or visualization, such as a chart, table, or list. +// end::panel-def[] + + +[float] +[[q_glos]] +== Q + +[[glossary-query-profiler]] Query Profiler :: +// tag::query-profiler-def[] +A tool that enables you to inspect and analyze search queries to diagnose and debug poorly performing queries. +See {kibana-ref}/xpack-profiler.html[Query Profiler]. +// end::query-profiler-def[] + +[float] +[[s_glos]] +== S + +[[glossary-saved-object]] saved object :: +// tag::saved-object-def[] +A representation of a dashboard, visualization, map, index pattern, or Canvas workpad +that can be stored and reloaded. +// end::saved-object-def[] + +[[glossary-saved-search]] saved search :: +// tag::saved-search-def[] +The query text, filters, and time filter that make up a search, +saved for later retrieval and reuse. +// end::saved-search-def[] + +[[glossary-scripted-field]] scripted field :: +// tag::scripted-field-def[] +A field that computes data on the fly from the data in {es} indices. +Scripted field data is shown in Discover and used in visualizations. +// end::scripted-field-def[] + +[[glossary-shareable]] shareable :: +// tag::shareable-def[] +A Canvas workpad that can be embedded on any webpage. +Shareables enable you to display Canvas visualizations on internal wiki pages or public websites. +// end::shareable-def[] + +[[glossary-space]] space :: +// tag::space-def[] +A place for organizing <>, +<>, and other <> by category. +For example, you might have different spaces for each team, use case, or individual. +See +{kibana-ref}/xpack-spaces.html[Spaces]. +// end::space-def[] + + +[float] +[[t_glos]] +== T + +[[glossary-term-join]] term join :: +// tag::term-join-def[] +A shared key that combines vector features with the results of an +{es} terms aggregation. Term joins augment vector features with +properties for data-driven styling and rich tooltip content in maps. +// end::term-join-def[] + +[[glossary-time-filter]] time filter :: +// tag::time-filter-def[] +A {kib} control that constrains the search results to a particular time period. +// end::time-filter-def[] + +[[glossary-timelion]] Timelion :: +// tag::timelion-def[] +A tool for building a time series visualization that analyzes data in time order. +See {kibana-ref}/timelion.html[Timelion]. +// end::timelion-def[] + + +[[glossary-time-series-data]] time series data :: +// tag::time-series-data-def[] +Timestamped data such as logs, metrics, and events that is indexed on an ongoing basis. +// end::time-series-data-def[] + + +[[glossary-TSVB-data]] TSVB :: +// tag::TSVB-def[] +A time series data visualizer that allows you to combine an +infinite number of aggregations to display complex data. +See {kibana-ref}/TSVB.html[TSVB]. +// end::TSVB-def[] + + +[float] +[[u_glos]] +== U + +[[glossary-upgrade-assistant]] Upgrade Assistant :: +// tag::upgrade-assistant-def[] +A tool that helps you prepare for an upgrade to the next major version of +{es}. The assistant identifies the deprecated settings in your cluster and +indices and guides you through resolving issues, including reindexing. See +{kibana-ref}/upgrade-assistant.html[Upgrade Assistant]. +// end::upgrade-assistant-def[] + + +[float] +[[v_glos]] +== V + +[[glossary-vega]] Vega :: +// tag::vega-def[] +A declarative language used to create interactive visualizations. +See {kibana-ref}/vega-graph.html[Vega]. +// end::vega-def[] + +[[glossary-vector]] vector data:: +// tag::vector-def[] +Points, lines, and polygons used to represent a map. +// end::vector-def[] + +[[glossary-visualization]] visualization :: +// tag::visualization-def[] +A graphical representation of query results in {kib} (e.g., a histogram, line graph, pie chart, or heat map). +// end::visualization-def[] + +[float] +[[w_glos]] +== W + +[[glossary-watcher]] Watcher :: +// tag::watcher-def[] +The original suite of alerting features. +See +{kibana-ref}/watcher-ui.html[Watcher]. +// end::watcher-def[] + +[[glossary-workpad]] workpad :: +// tag::workpad-def[] +A workspace where you build presentations of your live data in <>. +See +{kibana-ref}/create-canvas-workpad.html[Create a workpad]. +// end::workpad-def[] From 606eb6b3d864dda97edae08d932252b6e9b198db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 30 Jun 2020 16:35:52 +0200 Subject: [PATCH 13/19] [APM] Add API test for service maps (#70185) * [APM] Add API test for service maps * Re-add custom links test * Improved test names * Disable eslint rule * Undo readme changes * Fix ts errors --- .eslintrc.js | 1 + .../test/apm_api_integration/basic/config.ts | 1 - .../basic/tests/agent_configuration.ts | 1 - .../basic/tests/annotations.ts | 1 - .../basic/tests/custom_link.ts | 1 - .../basic/tests/feature_controls.ts | 1 - .../apm_api_integration/basic/tests/index.ts | 2 +- .../basic/tests/service_maps.ts | 25 + .../test/apm_api_integration/trial/config.ts | 1 - .../fixtures/es_archiver/8.0.0/data.json.gz | Bin 0 -> 193103 bytes .../fixtures/es_archiver/8.0.0/mappings.json | 25698 ++++++++++++++++ .../trial/tests/annotations.ts | 1 - .../apm_api_integration/trial/tests/index.ts | 2 +- .../trial/tests/service_maps.ts | 261 + 14 files changed, 25987 insertions(+), 9 deletions(-) create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_maps.ts create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/mappings.json create mode 100644 x-pack/test/apm_api_integration/trial/tests/service_maps.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2c49bf78c67b1..8d979dc0f8645 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -334,6 +334,7 @@ module.exports = { */ { files: [ + 'x-pack/test/apm_api_integration/**/*.ts', 'x-pack/test/functional/apps/**/*.js', 'x-pack/plugins/apm/**/*.js', 'test/*/config.ts', diff --git a/x-pack/test/apm_api_integration/basic/config.ts b/x-pack/test/apm_api_integration/basic/config.ts index 541fe9ec023bc..03b8b21bf3232 100644 --- a/x-pack/test/apm_api_integration/basic/config.ts +++ b/x-pack/test/apm_api_integration/basic/config.ts @@ -6,7 +6,6 @@ import { createTestConfig } from '../common/config'; -// eslint-disable-next-line import/no-default-export export default createTestConfig({ license: 'basic', name: 'X-Pack APM API integration tests (basic)', diff --git a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts index 9f39da2037f8e..7b99622cc4657 100644 --- a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function agentConfigurationTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/annotations.ts b/x-pack/test/apm_api_integration/basic/tests/annotations.ts index c522ebcfb5c65..e0659fe195f93 100644 --- a/x-pack/test/apm_api_integration/basic/tests/annotations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/annotations.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function annotationApiTests({ getService }: FtrProviderContext) { const supertestWrite = getService('supertestAsApmAnnotationsWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts index 77fdc83523ca6..ec93d2b3a3b41 100644 --- a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function customLinksTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 42cbef69abbec..400d0d294bf02 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestAsApmWriteUser'); const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 7c7e5a8dd93cc..02185b0761c5b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -5,7 +5,6 @@ */ import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (basic)', function () { this.tags('ciGroup1'); @@ -14,5 +13,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); loadTestFile(require.resolve('./custom_link')); + loadTestFile(require.resolve('./service_maps')); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps.ts new file mode 100644 index 0000000000000..64910d2b45632 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_maps.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function serviceMapsApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Service Maps', () => { + it('should only be available to users with Platinum license (or higher)', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(403); + expect(response.body.message).to.be( + "In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data." + ); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/config.ts b/x-pack/test/apm_api_integration/trial/config.ts index ca5b11d469c47..94a6f808603c1 100644 --- a/x-pack/test/apm_api_integration/trial/config.ts +++ b/x-pack/test/apm_api_integration/trial/config.ts @@ -6,7 +6,6 @@ import { createTestConfig } from '../common/config'; -// eslint-disable-next-line import/no-default-export export default createTestConfig({ license: 'trial', name: 'X-Pack APM API integration tests (trial)', diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e9360878b7bb724924c89bbaea7b243a555f13d3 GIT binary patch literal 193103 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
#xkG+Z- z8}lrfFkXDbJx%pQDY2o?{cx#UwSCiOuD)Y)6U#f?7c5-g!g0Z9W3eOEQ;0sH0Sye{ zllwi5@lYjhTFN7``&+Ii@2LaXE60f7D8DRKA+CI`bkzG^GJ`n0sne`ue1CzvcYN+>R&G zy6Bug?o#zs)vivp&u#rg137+1_miggnz|v#QQLX=6vJ1VkEY~!@f$sG;fKbTOOz^H z1+iGUXy zV&ul7A4I5+!&{DapS)Cx#m&>yF>;a2F_y-`X;{Yyt`8%0i*XuHV3YTN_u0p~$}#=f zgY}}7B)GEq37x=U$ndE!L%>Cs*!G-T&rp%+S?H+Ja>H}Qrekf22)D@!{@&LSt@YB=CYR^#Ycd>@*xyc+f zXZSOZDnp&)eu`V-oGNS=JcA2Xz%|$Lb(Iy@yNekB1*pVnIc1Y&!wKoJ=0}9wIeeLO z&8+5o+t&E|d-}VdY|e3>VYAhVj$i(dtgGsfZF^tMd{mzD>6Mk1Lt)h8FJAV*;O<%` z>y0d`{0u%AzS60;mN8K7cFp9|vCpPDN0*S~54BQQHKdWeKX#YMqjm@YKwxojLDNnD zJDW$(odK(Z;eI8L5A}^3>I~>w#Pa7L$ID+|pT1x94MUd&7HJbehM(Ll?j!l*5)CGD<2wn}G5-}5bWg`?ug!DXJ zh|QCXQxP6SP#<0jVyQQU zmEl$PvO$w(W8a5SS3y0=0$`rywGY(%5S%s#^XQwY&x*_P zys{|rZ*Sczg?cPmWT$Fo(tHvooU)M3%}#b*rTk_(-JACBPSvp_gz6S<3mUI=wBBVQ zav{_I7AgffZ(T23%*3_Pkh$- zKB!#^3zq5CU-_vYIta-M`LrjsX+T9z2BFUzOfO{3@sA6 zu~|QQVm%Bvc&=bmwd~KEH9@w5cv(6iYPU5*Jd+ws1TQaUcr$j7npH0?x$lq~vjwOw zp-PP|ydLv$k0qY!Xx%@rg>s2L2mDHMdF8nepRD?*jLDlbjl9`X^0)#g`(Ax(yQch$ zEN_c^A)O}Zn*f{Bnzv-<-3ZrxS49(cKqqQF!q=lB!^Fu95>FGEV^fIMg&1(^x@ghupkQd`b0VPz#CY=g<@7Fz zlD1jy#@A4g?ywo@-h9^eARv7?zST<|N-+ztF@MllT~#GokH0QcseEZ!nGRdr zC_C(>>|{q3YE79WPQ!{jUy__m5=|w1VdOH`WNFL-0$B z;HCz4cRRq+65NO8I3T?w8Gb~j186n7BpZHrx(Qyp!=ytlD;gS*VMwBix9 zF$;0FH`_14@j)#{ajPpw+y>bnOFi`VSz5^wR)K)(dDZm*(Mv(P|0}X=to#v-EF1B{ z#)NEkDGSmf^XEv$V{B@X8Gm<6z89WdE5$2ZZgD*%^s_dAtt|1f(T+{Voj@xnpG`O% zZc`YH9D9xD*^*ciG4HuX17wg@F2CECa&R)PWy9UFGg5VH1{*}xo~}I+uBK%ljvV&C)v9`|k*yXlX?R2gWSfSC@AQA5ulY?#Jwwr4&!tK|H|7+NpCSO`98mgYPfxTe3w)EB%r5{yrhv>q;i zYfzr-AKcv1bYmb^`+-@Pf+qAmI_%K<+NY5@yKUza2{I^-fgfd?QTkJG=D@PC*4_ld z*L&hmLRnowQ1fq&4oY+x@p6L73u2?Aso%^iXL?N2<~cmHMG|rLaC92huMCrpww$)v z7h$GLdFdAi-dvv!U^Q*Dx*v5c8@T(>?aVN=Z#w<-)PDc^E(TT7GqyO;PkiPW4LZzA zrkG$STgu2Yb&k&Us$kteoKEtiyzB?I`>8`bhcLcm{7QTqiD8tD@Vy;>|AnRF!+|PX z9O}jS2eh-@IjvA8YQ%{X0s%e+NH!QL1Q@CJn7BTuViuN6V5%1elxHU+UsPlU#UVEA zBczmi>KPLptn65^evScs$_A7!qQV)a6TB3K##LY67!aKmAA7Klw}7?JYR+^~86wdW z-8O`3Z>$rHRJUgf=zeu^%jLb8fDY+-0i!(!T!Jz~KUVtX$Iq5t-%$muUULv_ps}R% z9Lgap?U!H(uR3~%3wxDJfh!m`5Mw8B=gd>4+I|%1t{Er7{AAGmK}N6K)dLZ_7S!{K z^$oawB$(k7^z@@RjN+!rF3WYrK#8(xP`5=YJ^_C zV)tsNNqi_Xi^A+G6CH92A-KwKi5vgE3EpN23(-a5`*A}ecBi_^U7uw0(+ z=*`ny%`lJ89lQ-Q&eS)Nx;!~Ly?;&vumoSU$y zyaDG;_0WoT)m#sUAdN5L#{A^op-eC-VoDl!f8N*H^5dGdcxtI+-XnOvBaj42PUzYX zjBlEs0-LEHSPPsJz!`Al(aGg*qEY>^j8^rIg`(k)`v7BxFUgXYc^@|%4xhu$xA`Z!ktwoto=#g#Cayd8v%>p_ z46s4#l8D0C?_}|8bLzq7hU%I8U7uA@TGe9AV$*7?E4MA7Op9%F$V`f~`#~&dDTYj^ znub$R6{C7_)E$6B5Fkb zbLMwbWbY=N5UE6Kk0tLq;+A8ddI4sVwjVdBNYFeyeVLfNr>1*>F z(`595Y0876NwR78Tls@%D*tIBycJGk%ZqjG3*@E~*@e@6!m+68D8wv9W189_5MC12 zWipiRldI}zu^QK9d8^~pqibats1XiDfWv@ka5W{8KN8=_&kkxH7|^VX!r8M-P(}E( z9MLNOqY4=lOxz}qX%KKS5vuH_QC%5g@2nAvmrrXdiz%dm<}l`akQH+@4f;um^VpYH zR4~M7aIRoO`lY*YrI6w+i|nMJEUJ(s+W$*gl0&6|Wg}BWeY8ypcTCA34M`$5&4Qj0 z+@X#J{iMd>IVY|0qxRc1Z#E*b>t?JgrB*W(R}hxP7BWDKll^}w_RWm_@8-UKTQw^P z)68NF=_jYm?rY8>1SV`LRQ;Nfs{}1`9_O$sG155@X5fF}M~75IQ;?Mr`oAhucd$j# z7Za2P_y+gQqv!RIibblS7?C;glu^o4>5>Db+`OFqrFBY{L)9TCAHqj)(|xx<8H;y1 z>`ixhdAf!+W%<%+0O{TQuFEBb*+tu3a|}N|QfJ!<9I}a8z`2pRe_S~rU@dUjSq@E>DlCvuHb#imhEZuc+l*5p9Lg0f=`vB@5V}$ zvGB0zT{<{Va?W?|8;>*dv-WrHd-3x~E2WnEmB?w@MZ~Qc$!Je*w|u8DaLI*6b~mKb z%bo-*LUb>Ju9;nv^P?N06@)ZTt{`Pvgmn1R^2gz9uM5Si`f8NWx-^joPvE6kapKeRgr}}@iY0WmrH63-^w<~%lV$v z-IaIg%fbsL`@9G>V2HYa+{2mf#&lydp7wMKquax+%|Bi^dC9FURkxEt&lWXS(PZ04 zJoqZ*fSIgw`yS+@eASJ=2)C^DaYm%W%iXdC6M$W#PUq}pe_LOP38+<3O}@$zx9D;o z&{*Eay|i0V;&Y-kNy_^uILC0`V6>7V6@`~n&34TQS2?N|m7$63c|0gH1s*mPq2 zvOXmmm?Y)WIxuunxP{En&O3Io@rtz*v_*E~SHnE3_5A`ZD0j43N=+!Af0Rf9v1z&hkIY?G#+<@Lkb zGdzBBTXIJIX2|EU12Q#%)@y@YYZ-D}5IRB)1`UVu>NO;sXE9$oKs}-#KaC<8!!S3w z8M8l+4~@zky$P5KO7MSuSa|_XR^lM2e$HnXWHiiG(Qw&4c3yS1z8Fo%>+~~*5Xvup z@T7OP1&1BurI?Nk=u2-BG`!>XC%=bLVrqRxm8~@|!gqYbwMLiAwQ7~$EcjZHBlLQN zYpd_$5y^K95x5-4@#)a&N{$>7u5Z*Fq%BXZLTtZ-Z6UNicw;Oha>)!jiH6HAu1JmX z8ZEr$sS4rVWx&h>+4y$mDHYf^u%1WaI)>s4--sikC4NkrtvB`dJG2u7eX zWB3+_;B7S>mRvj;cn;doxr(HRCg8Q&C)&V*x4C!2D!t<`dv;beX+R6g^C7MA@3{5i3{3@*fxM9M)=$uXQ6>4Fy?vSDk z?U!js>G_t7)jG9K<(Qq)6*re*N17ZzZsKU@r0#^J&Fc(-{s7?Y&$KKj@`4g-k4Z(z zU=EveZeZOZTV8AZblOHZOkKnNn1}|*5nyVoTv*~Fj;M3m795MigVZ1~ z-u&jH^~jj8o;=ggbQVC5-b6IbX=J*<)q!I{O2@5nDNVM$w=3?<36{nVjLSM_PGY-c zDt_NQVN`?tC9XG4PTzoB!W@eVU1|p}r zxHMBR^82*nRDvg1||C$v_4#QuH^Ba%=U zo981wlq!>Ssz;=;>uCvcTN>C4&Mzyz@$B16SkJ}#Jf=~6R(4Z{vwH3-UOmsYpqpjzVFTEn!;)ynk6QTFi9=SAo+DYLwPB>}(siI|1Ex%{i@TuSJoACW4XWJ+NK#J%V-^Uy$nYEVno39(5`W zOg_62>dxhScSNZ9Qda0!>or$o*3UrS>A^rLFPaC>^PRnsG2`#@($OPjUOZ`iRXNT~ zYVySC=<)NPYNSL_lOjgd0;f5vr!#91O0V9k9cblFb3X8U0an`lO1;`CZN0+h0lh+3 zJl$n|nc+b#W-lAhcLGBE&)}VCm3IgFYTDqrL4D6Q-RFQ$7G5%17bn%C$Qj4yW#f5` z^OpH#C(2mm@aSdu#+P>NY&My~rC~DmJ#*!RMQ$PEa62@8V>zBbytasYEn1Xzp~J8u zFR@{2C@D=Ix6GQ#F4)9&KK{UArCa%0Nd6i%EUor2$>+_`R-ts$YbqQDJfO>E!pz?N zg@xyHV4YQa{tlSaGWB`(pBs8Iu87lRqXT8g(x;(cw7x8(T=G9`!u`q@NyG;CI z>Z(Zp_qTdG;qEEeag=zKVKgVQNs?Dt{*_sOB3C_DUbjzI3$3lLPAbwavM#`&)>!*+ z@iy$nZmVLmMQb|weWvUQ_6F+P{l@MdCWrE}GwJNn=gLXev@(Y3OHYCnAUJY`RjKG1 zXOnLKV`)pZz6r-uw3|$W)_nM@{b^y;M>-`}m6{vQ6L(^k& zQ8JIs+L)FGwQ&eW4onb3aFMgPP+F^fh66QSzp>Au1XKQN6I+`2| zuaH)m;YTk4lXxcQU&R(v`l?Im{mf9b=V|6+!;QtwZ)AzaF|DSxQ4Ua6GJ zGY7&Fde!d@w7IxC*~6e>v3^mV)Mul`ePYE7Qxdq2LqM_?x%yQPS}m4EtZ{ym zyE^wg9+vdP@|(e@UA}a-BPIi?mX(6!UraN*i5A1burBTsg2T)MGHj~FVebXVZgke& zvX4&>fq}ueo#|XVpPsbjVwYp)h7acuH$Pm(ya{?mUWaSV?37r;38obCsji$fYxX#s z2;Dh}CRlE|bKxbgQx#nk>;|QfH*!tN?!#m=#8m>Q3JV}}mY){biPDph70@+sYER3z=4NRn(VSGA6Exa50 zoEJH}Rr>}_bQZT{=_BO96(bR_oZg1vhS836FIO4&knGSnseW5_0<&Q5ys%*Iu0IqJ zq?^BkC9rs|jJz;?!fV{)GD25&Z?dhtottIEO2^DkCOY376eZj2&~MV9ye*wME<@Mb za~VIN^~`2@By15JDWh8${&o_W^V3xx$E^CC8?nPUcjP1$ld;jLU1fL#klss@zu)0 z8t9p)0h}o8z=>XV-gB&9K9Gz}6gwD!MFw!tt+p;$2t#!(Z$Zco3awI{MKVrHiWMo& zjSg@%6*28ywyd&k$2(+Q_*(E>i1G1t12VuSyiBDsL;v#QSV9o>%q*hsBYmC5y?pX_ zsh`{i5j&))g(O?i{9or0QHjrXBjbpGZ*UX5RST;K-9-uqbQ!+`wb>Cr-XW(Q!JEgG zpRE0IborqDrGw}j{4d9_EUg!R+0S+s3q)<9#NvXInkvya%QfFU1v=i;r(Pw*v!mVc z&_=*RLb=B*FiiS%tI2s@^vv=csKRGGiFZv#mh?cbjQB30Y>5H^>p4S-Z&+`fsde}e zv)NVQti3xS59t+UMDnBQH7z-9+3PY)R5BwIqmjIHObkmWU=>|CXrSUi9&KuP?;nrZ z6wNc_tG=gK!CA>6ak0qBE^EOK5tMz$=06#s$j*~WP6T~W9m7@yWlKbe*b5zm9_fMGt}%0{0udDqE$c{!)(xGD@=AH+k_A6~(-x4|jfCLO7bWENN6-fDi@ z7i`4P~rNy|#CYx3#O>CQpRvjht)-qbI0Hz`Sm|tv-iQrtJYEY%`(^MyC zoDoNUXD;msLMBLI&=zBjNSb(Ya&-A&!Ii6H1qt{8$H*Rl!%4N0G(1H><_)16(*dq` zn&@Tvv(L~n9Sh0II@|0OpzpD%5q8OGOb6ZFd4fw?Nss2xWy zwE!n+gjpPY7gwqW*~RR1Y`d$RoiKp>(~iDK&B5mqdDFP*mAgMakZG`oIN}vFGX^Bm}xZNBz@jEO+GtpT@~#6+6Zar&<(i z)M75yx4? z%V6I+y}g-f?UByAgK{vbRMgCNas&>jv5K^bv6)#mShi+4sW4C(ml@HlR$6HxC$~IOFN&xPCCoNsaz-Z88IAn?0`H{c8CCg<7M|$>68L!b2bv_Rem$@+t?c&~1q7)MNczaX^cn^*|!)t&e&+>tu z#vTN|d$k10yk5pmx=IRW-?TihOXIDi6NaCrliiDhKWe+D&uJP0-RO~2ecyZr`Am>P zX@j44J5<~=89RL4GiH^HldgQ^Ui)9d`M6eOhiK-WrpxG&xMg_2l%J=cL6dLdgrsjf zMJ1SecbeHDpRuM7g8PS?FOUXt)NuMNj^MwH1fL~gMJj6$Xa+t_+FwO{ook4^7o)bu z{$M}|f&2~Tb0DJFTcP{O>AWtGp_KP=cUxZ2KL1UK{g5 z=s;{PG0Y+7xljG|?#nCGZ|{D+1($mNxkMraL~9<+e9k&>Q{^twELX6(<9dZQ;BoUA z_IKrXZ}jn+SI%7aOM_QcAP*-8^tqnja*rYBx6+1~y!5g~X5G6k zJA$#x>tP93HXKiwC7m^N*T-p4XzSw?ylK_*d~l?(yAAOH9d=0~=IRN!)@7BK=enLN z9{+Lw;xpif?x^e72}svZ#k;}o0WbgUCP~$_!%c? zJ>b$R9NlvhD3|U1iqUb=@S)RLPR}9Nmd{e}raaZs?6IC(7*Lf0uiQ}X&UM&NW`k(E z-6V2eRT7~u2O}IDsxnev-chOT*f9oZ=Mnoz{WirS4>FvHD!4hJb4lJJfblQztSzJBadfywcp&&P-abHI*v4N_)T-pIIJ~5hYDk@lA!s>sbhM2-e-3$&8JvoSz+R1G_=$3e~Tq2)<1iqBgS! z!&nFmPO9w7kxc+8H02>`6WN|qV6>|?AZqK`)>&N2*)YbfJ-(gkXahJ7PU5U3?IV&m z#b=1(qwjB97LMS1lI-}TKu|35Q} zUd=gYYoG2@yQ+3QPua~g`!#(8XTdcT1)xz}@>HxaL8bQPq@QQ)MrAtombjodTj!Ek=2li%wg0&Vj!cM&GCWWBaK zy-9v}Fn4j@-hOE5c(pYL4UJ-H;SuO`aZuWY7UORsu%Ae7;?AddAJ(6>g<(()j_eB= zf>yllZBZyrDW19jD_NvKgq-v>v@F3`?K_4sHXe6A3pZ|z*pZ!uxit$s3q7nD_nO%# zdzE5gsqncqLheCl`4SfIokD z=?xelhrEz~1+ta_PXCCV_iktv|NW!n{!w<{eQ z2QYwXV)LG!_;vfgOfuc6?n`tc!J=>@s5fhRH;bT2TD!AJvTZjRKwl}EVZ3S1UnK1- zfXTJh>q)bfll3ujc=Y4maG(xJL}1-i|EK~+5w`k^F42qo5e7aFgU7r83%4y|QvXR3 zK5yZM5Rvcd9V2Bi6e9sAay%e`?P)nXWINhG{=61i)~g2SLiHZ-47^)tmSfL~4)m{5 zJq@#zX3xqF^!FLtn!{VFZ6x0v!ztOvJ`z=LDG05T)NJ`h+;UO0;r2gnbie;{iyOS@ z5}8wu5Gd0Dy%94<59=5c-o))HF>8_z;MvZ$9@m8k-sFC0fKSaWs&g->V~S_|7`#c4 zcEUg%A(vCfp(1(~C9)-8TD6X5)AlU_GioypAC!TSxQ|B!M$}e1!Y2Y#eHPgZ>h%~L zNN;jbruR*v_7#(V9C;Fd=al;Bp95tM-CrMs5eCBU1{+@ERTOiw_*R4+xtd8yu7 z8MPTjZGgYxQy(q2C68Vu5j|di`qP9^Ha}^a7TDx!+qO`@kTq%G@a&ViWj8T(ZOICp z(<02a%{l%SVP@11dIE!tk_0U18-ikQeQ){L}1n0s3;!eHD+$G1=ALGI!B zX=}4!=Gt0Y53tt{{<_otGt53Ta=!8yn=;T#0%2GIu(Ju{#r8Q!?enz;D6^qm42i&p zFA*$!H3l}7H1+mbk%9w}ybV%Rp*~;psDOTZeRYKVJzbdctHFmJr+7K`&;DGd-o9)?FjZ0Y`uC&Fui*~|?;PGWvVk}R?fZDucgQwKFo^a0qLA*;WM|Sg z6eXiAQdcW~8bD*-z5ApxRDX=WTQCK~D*`4BRnYcgD zjRxYj!W9zj0MrtxZk)&>870G+eKpLuJo-aIFhv>9HS1Eu1O0RBX2y(n3mg;Z=nBec zZa!ZA1W6wqGG;UpAti)Q zp{Aqs*`Po$9eS{u7bKD@p2Wirt@<_FzSi;O0_x$q31i%0K%*%aNl7J8j8r0#yJbMH zilu}gv5W!Y`c{ymNVHi1>N$>4*tr(dsyQa)w1h&3k=ki$s3(OJF{wO{Rl|(u;fJ0{ zn^>ms01fIDk-a*jX4D}SO*#+0(<@8ErdjQ6n`4vjN& zQ@_ob6u(3?%XuURtc0NCM^n^XY^s5FYG~NEG%ef9!(@1`^-Sh1Gvb@|UsX4w77{JL z4>MXZ`Q9<9{mB8-uXbpxM1}D9gm@86q3%NiYiL$M3DD2; zl<lcn6h(F=}B{{QZ1!SGczoXuT7?V<^+{dA>PcMp0 zNGVyO(>$)%hZ?)M8WA>QKy!URDm#H__SnDgTG>NdQ)fGc523QueIREefVEQt= z8j*4nFa6T15r3ZwDF>xWb@!+l3>VbAQLXBJ*(5~YP%nZr+bKRIeu=siUAo!f3JD?|E3rAU*Ol@<7?dp(=es|zwk7#V@=HcU(VjYb!CJ$Mk5OvNFg;z_fiKO5Ev}ma zmEVlF_+u|L77>b{Q;O^#f@y3j*;M2H9Zu@(F%jn>Xc z1<^JlIe~kz(l%>G&&yzJyL(qF7O5k*(Jr%xO?7}R(PlHarjw3g%*lsXDiioo`7|z| z^Jy46XMP&RNN+zZ|5CiY!-g?GUIjY?~|Gm9!PcSOdgbqJfxTrSZs%feMiB74j$6*5sSz`>cr;b6CWza)0A<;zp9t z5J^5YQCejFn4O<1OYDppQL+bFt54?xdcT?pi3qo+4=J>8bS;Iv+(bh{BRj#Y_)R%{ zsrFu`7GgXf23jhQ6CKh*17n3+fTsytFiq)?wvUIV6iy? zn6BktPx?zuo|n-0K>P&)X(Tn0>lZ-eh`steKm5{Q(= z%Y0N_D!RzA`H`}qr0#9b(X_dI{c>X0Gh+Gp0R5={kl2L;&W@}ZceD;maw7(%;3Mvn z=kWI_%)w%C;y$bBGI{bc{ODavr+Co~12>hTnzj`R(X-vq`Z>OA`MO^p)|?+K%hYA^ z#YHo|Op^@R#hgES7w^58O_Xon`(rvj4jn)o@Se@BxQpZCL8UaV(URSYhXs}xTWZ)v zQeoP-+qQ_aa>i*Gg_yZ%V48VbBt-@3jVag^o!arh?SC$Ys|gTZ7qwtQ#Iiz!^8*kFTnyUEWUMT?azI z3A^!{1jgt5eEL2BR@jYu9cUf_piI9Wt9*DZ2Id7Q)A*b}%iy73&pUf)7(e;^`uKA4 z&KHGU`ol|YF9Ue;pf?){0f&GR;*Y<^3mGs#Cn&%%^YUB4e?1BNApAb-oh)FixBft1 zhPMeD!#ypJ9KwB@$bp5Uu-k#Q!|Rj~aQrVXUT168?cWKgepJ#4S{qE<02-N?;oUoE zq-n&S`F-{?9c6Q5u&l+>lelHHx`IDleKQwCWFoz`szxhrSsm8kdUv%U?g+lZWI^yZ zaQ6pPVYkJMPS6Pqu*EZWI&wIoEZJ^X+gyB3r;8r-(k&t*`h$ctAj)aas zS&?~%|40fbbI*6L?gHr_KD-YtK$U5U_?#b%qFB&%22i}mle6GL%XCTDDBLt-S7jwO zDvbl=!dE}|-e|NNCxRFnC!p;YPo%+7b@-He^Qx=dGsygqDmNB_Iiiz4b^(eDwV1iR?QPFMtXAr?a#9nL8aTSeQFsCUe?+2zbL9{4=eGvJ}mi>TW|fFV%0Bw%|~CA(~;QOh12j6(M0t zf!F|p7G#YT6-t5-EqlygEa?5qfQ?M@>MwrC;ofqfj>h;RD}$AVo8RY#&AnB#8`g?P z%G7_zTlDq1esP<;ym7pKD+<<`8zdu{Yb?WrSC^cseK z{n@_Uo-{i$HSiC~7=M1efTFj47QClHU&elETB|_;%UY{;#0K(qr6y>J=XI9#OQSb` zCg6~57nwv{;E-!aS1%VRO!cDzmpH|U<&spmpn^Vk@mOLBwpMC~P))vI zBF(OG584m)G!}uBiDY{aE6oc)XeYiEXsh+wHZYj7|?dfS_-rUs0qjs7Zimx|lQ^5liX zj~4*B?NPl@Mzk(4#l!>I_}*Q)3#hZCS}PsihmSU1L$41^ji0v#c*DnZ z#PQ7})lx?%^!iA$9l|pe2||v(OQz4uChj}OcfY`^->(`QS|k|8XLj3!ZyU(1 z9|omrvU|opoNP~xoWGb}bP>s)#x<}(ej~jffB#~6LHyqC96wL>vIo^ku1buk&+ypc z&{GxeVg{IAGhbI~p&SC038Wwno_2(_djOUfo7YnEhek~(LWu2-z7YnJS^b!h*w-iMwZA`4P#zE&;1nSCU&RZ!F>y>WFb^O6 ztMGqE^7qK6-1~el!@mrBX+2x0xVW8N2k9fQDVzbRoyB)Az^6cY4_~Z$_E0QdQR7%$w;mx$lyH0%J+qm%O}o?yUali z*bL98jm9DxfmV(OJ5t+X*sOv-pdy^X0&y!&L0`wfHo1C$)^y{ZH2aJf!w?XmRvnac zsEOYJiyKh7m-pGzHA;NeEwB}~<*Pdx+KtD(serZd?p??Df^ODc4T_t->Mohz)5c8+ zinJ`P1ZSPcRoy;kEtPtGfTL{!VaxvF5s&Sm2?{2Sq_p9;Y0{A*h4K4Er%0B@)`_Ad zFMqb7flOuzv7>|u;01!DQw!FOMo}G>=W%Efa+Bzd<M)fduG`iCt#`}+){6&KPG)xnDk2~PErWUo=>_uZgSOd6Wt-6VSG6Qf;c0S1 zvSW7^Mot7gAcysM`F*yLL42##2WOYw`}jN`n~s1>_qJ$O72td`aI{a%sP2AS2yo$Y z*eEh@6eHAAxc*3WSqgfIi^upb`DD$+L@YjKzXRTVQRMH^aPuuCDc2QN3Ka0?7?!K2 z#;#!+aGfu@@#+54MYO0$>prvw(XCnT4i`!x+VOfJkmF2JBU^v_cWHEc#5$Qt2}wn@ zph)*U!dShGfPCiuiuSFN3bi7dx#IlM^O6QN+5zzF_=`a4+^^9`)9d+;-8D-A+&%PyXl!Z>X|1n*%W&PF*4%t5mH3U~`|AmDPx(Qt=ayxCsDPB_Iids>e6Uc;CwfQlOE$$Nv8X7UT zOJMYP@>|B{(!Y;unzFH+5AxEV;sB1_&EepM8PoiiAcA zvsb9kDebA;a316ctzbXCK1rO zZ|an*)_AI5pF*`-O?*_4|McRK!E`!p5Gxysb9Mjn{l>*HO0lYaGtk=c=k6+n;qUkQ z3nnGVIQXj8qrt?-94QSFZKxC1vw1_#Dh;Hf&Z&c}ELGtgUBZxX2$QTz{*PB%&JTvm zPnqz84aNLdnto~@)==7u@6jwJ7|_hfTAnJ4MxA{cOk}=bLATHO=!Vx+ao-V>1gk58 zVy;Nbj6Sk(+7#m0lsY;CAX;Ly8qCg!6%Jg%Ebg+gHEf4#v1%nX%igqRw1r<$71>?u z(&J2_z7YaTfF;1u;~Mjhen&@sXv35HL}(6#s0fdwAR{jzgM9n`4Cqf>9ao*Bz?3Iv zQGE`2s$s2Mio=$jYg^>sq1-PKb-5^`<{N8T3rWghDy0}%ZlN>YQ zr8h7$>8012sw7!9qEF&sr_C@PmR(iJnBHTztB;9`bnBBb82P6xm#-&4yYRh9gov z9&URa`%-slBioa1P9{FocZ^HUVwg`QH+!eDCSL6;;xIT`{g!Y%AT77)p)^q4C1)5r zA6GxCsHetC2f3eV^}gO)KUFB5F=3>gbqMdWUbWvfJVoH@4)~MJdO1*Chk|A%nQo{4 zJXP?}!--Spy9xeq#&vs`Ql`_kCx)*K-GFWI3Md(4Qf_zLUAH12P9KUvDhIwj@>~Al zgB;C?eFVmVb9}`23ylR&Ot+Px!nqT3H@uSYWRbuOyT@&@!a zWq6G@WqLEaosq=k_?p7v#fz1CGYii0c1k@Sx0dD{VTso6F19~j)10pbnZxYIOZ_<4 z91yoL$qqQORoj-+72V|Qbq~A$e9iL)zhS-(z-80Xy&n*?fyWsd$R;$ItgX zS3U=zbFdXS`dpO+hXME*#dIoau+WOP?}qsKMj9siz{k8gr- z)rk`HvqGXbLjgu);)3#A-Z~W{(gq7kU|c?$-o2JT_vGoAYPm^xey-BBKb)IjDeS!g z9x@udW?`6Ev|bljA8Z5;K9{gZ*u&>r7V6so8a4%2>mlDT|F{d{L`3W#xba!EA!zV& zialOaha=(A$fVdw59}?E?z~A=afCe*9@E4Sx$&Xv^LXd=#e+hsBA>%-z^$f6s^hiXB%-|V9=aRXElBD55ld)(A07vv?F3JWd4qeXO@>ce~ zp-&E8SaM0#k2KWuaB1ZPR+DWa{z{)ae0HLQ`1BReIDR37%EH36JRHr7`{t*c(k<3R z5rlQ=;MRFDPgiES=!4MXq8Y5`(~n7)2B;KOJR5My98R<%u3}p}Rw`qaF7%d(%O3wF zZN1;8${63dbs^nscv_AXD9kiMm+T94C%YO=-sHS8k<*YHoV*WfyD7**C(c6uJT>~m zIb8xWUrN}WcMlQweN;J|B@Y_p+Vmq#?Oyazc=B2Rdi5`VH1+g!+dbCml17ZzO>awgXesY|@4jhS5idwizFJ z5dR?!Lm-~C+iD;XYrMe=!2%123o`NoU*Ii&NyEj)j6E)#X?3{}smcWssm8PV=2ek@ z09!sJ$Uz#{9GYq@B-I)4?T=%ZK3-MHw$raUz#g(mM$4IuGAfAfbhG8e3uoJ^4!~v2 zxk{OB5&rVIu1aZTA5KvO;l(jGqga3gf#y9E(!)>`59kk8a!4d79O6s zGX4XTv`D*+o8@OxWk58l z{(=Mc`%Deo2AVbo1jS=4$W8abK}&}YlCknLKDV&fveb9A-ADd9-yw2sun&B2pT7NB zhtj}bzKR>4M{~%R_UUF~XE)JQanu4Ohgvf2;WEtQvN%xr25pT3pVSDUNhfBFXK zuqBatRDI%c(MT0Xw0IrEVWB&g87{-Agtom2dBMqjTM?vM%DoLC&z<`a$P`EV9zX4h z1>Q9EQ!UvSNwW94wJGmt7HDG_V&CDam=LSjj9U-u#YCWqxJydK1{ld5SX8G&a+-16 z4AUd3j;lK-78 z^t@qw@=#nn$U&x2kV{iKP9lAhgK~iPg;E{cMD2j>t27!xpd~s#{jEZ94@;;uw~0Z| zcppC=&D2Y9qma`O67MAk4wCBU5-NZwkt_$njN$tM)MQRN$WlO zP>+AN?e{4}7WNt7u!~|h;b3gEbu`kG#hTlRMcP_lX~CjcS5XfoQvdV|yp4y(ez-nD ztoGYSXZZvo1x(M9*7sfl=EkdZ5|ldIQ}f2lS5lJ1`jdubOYxkmh7ywMS-kAQCXfwt#B?6k*@Eya++*S)^aII01zh9q!N5 z^YAVKb99sY4S@HCgGlS6xwXSez!XPBd#(jd53TGZrIaJehLOM1rPAq7f+3Njkhor+ zP^||)S>-5h#Way3qh~2AuKvo059Uq^=I&B_j7`uI4cI|HUnh(>UK!5DC-hXtfA~$n z)qJz2aaLOD@nFEy-9c2kURvyMRWy^}S)06M<_w!tHJ5c1p6xC0LzZ)dc!;9T`$e1p zq*HPoa@&hw$*J?!#m=UvB#IVlc5x17?}u<*WDk6!r_Ki6EV!#QiRs$N{4&r`Og?}S zPRlzk-xp={#7T_f@d>81C7(ozWcc!;FLEFJu_~@5@hpbIf7p=cM(-O1lei9)Q5GO@ z^h=T31}zlVMFs78@;Z149&5C6hOn@ZkDV42Uobv+e*3*Js2&Oi zQ4TPj)c2SLXxa%){QS*evAyEU^gt<-n%MPItD{P?W=*VH)n>?oEewGNW)3Gr&T5FZ zZ~wuG4nRk<-klyKFAI=C{@z_b;Bn)7zCqB-UO%SUM&p!d!P44XXq&KtY)D)`nS1v< zf6N}(jSZtphsyd`ZGGRDn#(1Wsbk`Z$4ZCuPY9@m>Q(l4-6hoh7Uk=%2C8ZI@}r;- z$`jDp-ESe3f4%(&qZx*yoe9a<@NWfP@#NodOnQCdN%I7g_NLqQox1AmVZpY3M9TQh z3J&v+W(o2Cjm^}&ws14}-vwt9PW%H30{~}|-I%Uy#t@!N!F0Q;wH2576)vf@`9hMz zrSu$s96*ll&%#$w&7~hv8NRjzDK!t_XhME?Jgoh89>eTF#;mV<%$v&uZX(DsgnY^a zY=MiYHa_C+7OzbDJ4Yh-8%Vas3G1eJT@)W73T=5tJy)BZASKb)EibM%=fl_6#YJ>&q%Ds*fo5|ci4L^slHZQHWaR$q0|9se z;9urxAxaq2bHerHs_KlyCWvi-#)s|8Uf|?mBwr$HX%|i=1a+2Iz`l1|sO9PG2=HHu z)NwY~x493xAo{eL#@H}2+=$CUpp5; zxFl24*{(aB(c$ZD^&|VSJ$>%XZ;fGw-M9C7ujy{*geOhE4~d6$R{YXKh_Vv%=r<4e zOa1Tl2m{yy@rGRLgS9~^f>-BOZ&z;jDpIQji|-@v_;>1YFZ%>yX!p#m`OU)3zBb_= z^ZdsS0NvqGji2auUB)3ghJP~z=Dr-qFH0sdh32q~UTR^ZEc{UCZ$7fdP2i$(JO1o^ zyFFD}8rk}^(~fpz(1IT~A4Wx!nk_pz$wq0n)+c#8-r4>iBLG|kF(T(OOC(HAEa4|_ zt@c)UEx62(YLydyb(Dp=f+wRyzLVS6Ecx1KS+vx?MMLrXB+&3c*DzW4H#wy9FF6zu z`yV+ps24o7wa@tvIpluymmKQl{9nnTJ5o8UJtFmp!;kUe=DFudn=F~rt}!55xQ(2V z>dp^|_Xnb-NU5r-VYrKV*LvwPOrcF0uoQ4wraUyv9!pqlqoy=E=E{y4x8AP zV7RTwWac7iQP*BP2*~4Qq6Wz0g`qWvDlsQT?J|F51_8+9mCJbwQDoft^vB)dt_-l&>TJG~tS{_4WC{97W+NFD@N zS=8NB(((L2;;vpPcTe@+;rIJ&(M(VWb2*6%LJL-u^_%Q?o0$E{^T3+zJIDB+IBrQ) zLS7ADm&`}yk;*5cQQ*H;t2GmrMdb_&_=&{pNYAM9ou)dZBz0}>=pbN;3^cwNZryDU z0JBYF{Xb?~+y&nQAYq5xqDm>-(+0F6c|Rq(#b_Yk*Q?dOB%15COnpYLJ{2xEQq?X{ zSh0i&4E5PO?63vRl}}PP0RjCKNdD(CwAdw{=6m;w9|t~b9n<#Sq^eb!PF-4lY0Fk$ z(aP+O<>2&r9{r(1;sg6jc2}udU8r2$e~XoTCVdIxaM}1;P}u8;_^VJl8$hyt=f$`D9fI-We}KX}r0#rvK&A8VFw9l<1v*T=g%#e@iKKvlRnb;+Qs` zFu1q0pw=%VCNTQ_&3XWq$dhTq0c4HBM1Qs2-Yke8pa6U*;OF)Z2HM*AUkjfjL)u(T zt-VFiddyI<7+dqz;vDIxNW&HhXaD44Sz{v=>Oq>mknLbY`fAY~@&RGW!9TV{Q|0B* zz{Y|3l(b*h_H>0sxU^rfwBMs;hA``jam*jimnZ(?%)vsuY0G(YGI~1B%drvt2$oif zZF@`kA5GJ!WIUV5c%^X8zP)B2!e=JnoI`Fy6~Cn_KAk^(x(yA}zT!DMmpB50#$(Q; z&7nutz9Q*kIeJ$qm6VJT0gZ-l1b{7x7m&+oU%js!QR+CL8yf&AmIAy2oE<9(7n?gUl92GmV-x7_ z)>I^0-EumC&~ATkqc&exDO!GDcaz%R0=WIYO@J<_iZStjo5%&cX6}w;*3|&f+ALI- zc(+f7NkN8Z{xlpz|Lw&403G%(d$)1sf;)+}UZt7c!YT-Xu>ScBKKvN z`s&9IOmHs>W|SAkE9A$>`nti6NR4zQPzrf`FA8C4_UFwK~fy;pm}Zg!BV7m7Q8Gishgw$Nc_0@m#&SK zMWb4WVpfD|7bV#N#~(K~nkR&Ark4#yu!caFt78v7JZA?~1oymGD%OWRm5wf`#pGF; zL6xAVQx6+rP!WNxJ=#V$^tTRf;Z~qhtrSj{fgMIb_hzl<#ON-}#zSs90OFCAH{uaL zh9&&lgS(lHcy_TsrVIv#GZmeDiT&m%Y4RK0HdM$K<&&;&R48uc9UxEd*wo89K>obF zK3?pv()DN++&&79O5ptyaEEjpW|zsWz7!YiQcHm&Jo1XTu)9*RqIP9Di({Z>V#G ztsCvetORYUAi6riwsr`{w(di_&>35|_?i+wIl2fMpQFi#QRPAj_m>@BCAaHpbhXN;OLQ?znHn)$nE<-AT_0u;`7V(HQWUA9;; z`(<43|46;CcS|y0wWCbH53S><;bMvD-!a;r36~Xl~sj=2^O>F`@{2%8p?#S)?DZ@-OZ|^~ z8UFs&y?m>~e*)bHE7F|K+Mt1@j!r%o^AHt>P!Ic0?UHWTUAK3*XqMwzMn0Q;YCbzY z*f~;(=I8>xE<}3Tw~VXM#30Mb>B!DGY0)3|)g`^Nx|nNTy}pdkB_Ah2#?|do5ImVb zC`&5vtN+!f{H8+kT-o^?5c!=E#XM5$bG=-1vzU`AO=vxY=@ht=Hs{!XSd0+-3in2GRNzBH ze-`adcOJ=F*3o#&3E2+{9rGwwk&ZY=s%JCL0K$w*h%P2{?=1V(YA{s^AY6?16ObT9 zbAq%m)O5DUVs&oPW1~sBkVCDJ*YZ1&1U|B$C*A8}rVvJik^XuzoIEb_Y@V^wstN)v zt}<*pz0a!a*KVZASqPUU$&2+fiVsL(FCKkLq zO%9MN)|DC~2Q+jW2P(qIlVpu*+5c14Ke-z^QO`rEX!uE=u=$eLCN>vN1zND!A8SVS z$N&UI`>zB=J8NkS=SdSwZL}BP5TDYwLs}H2iBz=~eR+A50XD)z)3EM28?W{HMGy zbjjPRk;8as0k{zOK#) zlo^>9&BWpO-OeHaokhu)a37@~|5aUC#}?%o>A8_>$3#KsU=3^a*s2vMsVaDT$lfax zr!n2*fZT;4dsyt!SRyrc4Zh1#QHsa% z3%pd~`W&YHM8mCG&(?*}Cus9v4I0B~!P)kz^)Azr>Dx?47LH%0$F&pkHoyfKP0!19 zd`Y$Pa~u$BTRs-EpleGw5^5 zJz(?RflMvETn%mRboxL!I|ZT7)-ax-P3%CCGk>X{v7vHdv1kx|zKC7!#tAP){e0=D z&N1GEu&Gz2x6VO!^ZrnSvYEDqR!a*=ExtS&E!lndfX@hr^?r2Df0)zUpj**UT>15% zxxpW*t2du?S+q0XZv?gY>mom0GP=KbMP3x4%0?MLN3Xr9#gkF^7Z1+%yJZEu{%>+7 zt3;mvR3YT*^zQ)WLF3^lnBU8XYte@lptb(jN}WY_na@$RpA8B6wy#d6I8D0a2I{3l z6=LRkc>NyA&jmS$gGDn5DuWHui^?mEbBcELawrPaE>0J;T09@Gj!T!ZrVU$Ws{#zq zzA!~%l!Dl?x8UXv`EF@x(A_$r99B%-ZBuNR0YHrj-L=^2b_>ka!G+29-BxmarjfrU ze?G2qP7rdYT(;HUxQuJuwZZi3=;$0;UD_GFZqUm+fbq348zJke8v$&^4PGr>!5Tdw5tnFyVp%ttH$K_GU z1b1mcJUIbtwS=~PT?pMz;i_VzAuC|`Ma6ZmI56@l{S_G2Eh21ebFqh;? zfS|3KYsaXaZWCemhQf^hOga~XM)QV@!_h_y0zZ!9|2ZleJ92J17%AJR6_Hto_)paB zJHndAEaTV=RMrcvukKVqLR0(Eq>`RQUb^w=Sbw6i%6J<4KK@5@zeC|~Ni`s7$C%c?n)b#a*ln%*ri=sSU3 zU73IxAwq$kPZ%^((=<|iCU?KicZjhUJJPa094~kUpT~Eo+1lpMv9ka0`*ss6FqCuY zVb<%kM~O9Ou9~2y;=anjp5EM1b&1;iar~%x*HEe1!SN=yc7t$w9&6|g!hvR=2#yua zQrG5aSlkfOs(Zp3nG+#r#o*D3HUa6H3&r&?3QLJm1wnD?xRLryPg%g<*j+zkZ<1TX zQ`gd2ZeF@OWX{Df&BUbRR2bARA)K+T#_pJAyYW9RkWA5P1S(T)@|HjjNTf_;l0N;# za58lO-l5St5gMbTcOhzX=hKDYlzL#L6Y?Yx;OU2b<8>Nl-#pQMpJEAQPQHfBTI~Ai zIN0_Q@8?owPm|$2|s-NiikXpj5xHyD*hUc5LpMDLMFxIu0 z|IIP;X(T4a9x74f&S37th82ou@`|L}vHQ4*R*ipOD>+%`770E6JvR^7+_ky2<^# z$_|x3V|LAKM%JaIsRf#sE2t0EvZLSPk}J|M?T)pcWm7;v$V3DQYY5C%Vw91zoXnylSGJP=?PW<2A8bKRc z|8L&PBi7`}+tAnSou#blXa+h3jGI{ZM`!46zwhjUCaZ6Qz;*I<8*630TAWqYgH z1Vv1Pq18oGRCwbTTCticIIdP5tZ!|zvD~6^Bx-#UQDWI@e6EFJL&+A}41%KVRi(Oq z#tzjk?pn0)h+SJ$e+^0k>8Vl>?G*=c9*jAv&%2DkzRKY|3^^POhO0>5U?L!Ap#RB> z9sET;nxi9Xw)}sft*m==s|T=Fk~d?h+A~4I&>dcTgvK~yu8e*l6D8~cof*M)T-v$v zC1W~}EE(@)N?PDh&(2k`?queP2Ujg*uT&H{w%d@{E?QbpP?T)!a2zhG6NR6Do-Qjy z%IPYqH!HD?ePk57$1N+|uA_|GS}c#+L<`j<-dH2EYhzl0t9;?6444COQzq)h71%C1 zIw=h%gm*W}KNwRVbTL_#?dQa;W`>4NL<)G~)a5E9>M9PmU|=78_%U?CPmQ=#gKX;f zfJm5`BrJ(J#37<$YVqLHS0_zxA3uxnOuBMA@9=XO<_Rq)jC2t($u`Q5T)HrLiv2P- ziu7bC|AR#F6<*jb*9X1y-}2r|$@1RuH-2RG{)L0lsKc^_6ZBsQhh1<3DErA-BIRl8 zYYU5#9Q+$a9gvhr=+z0-x0H@aw+yvJh;wlzS%?oPqf~K{$zZ%^IMUVrL+PUIJ@B&ax0 z%dzFfAC%VRvj;jzYZi{V$ciu*Fgj}tuIZJQcZ!x#`!H^(WptYkLB z>FXzH%@rX%hko2VD5E>Xq%PwC)bvPhncn;wbD_gs!x^+*G&7PX7tvjr% ztTwn0++a>mXeg>$XU#vub&fR?bScnUOgJKjVU**ZjTcSRv0-g z2Sii9k`_qjg+MC1TKx_)5_E$`f|O4WwidEE+&URdjr^<`&0EF9y8Ed@l#*eo;q*4f za-DtIv5xt*s)Z;Rah{Lc^ zZKZ!@P&1SAP6f4pZX1`YQ+62e$YX!XSAa?Ag5~=ZAwap1jdK+mXajCQeurBvTYAtz z&PBNznqK(f!S^^3|0J^AV#Xh2Yer z83K)VVBMUt5kJ$rJNN5OeiS%ri&Th&nygZ7&}jXXrD!&b^E^BjZR+UXTp)=$lIoY6 zje*p%H4h(X4!e-&Ui8kbx2A&TZV&(v;X49ih z_)sCoKIXFVT-|Ji@@0$G!svB@=Oarx&~U#8(31`H?qn)D#Z5@}uVhBj!mQKu@3;2M z#&uV@5NBSN*d%NHPTZU}6r428lvJCD6NI}ao~912*6at@Kh=^~ReM^@airFA`ydP0 zF%i`Q8h6Yu*1DWyLb(!eCK65B(2p;;ym8%^Z$^JNo7t-uNg51q{RYNrX~?c-bCK=8 zoEo9zY?^=CO0y@?56u6nV#;wJgQ}wQ>W}sm~bJwf{@8~k3FcN zug%GZ_3fRyyn}sCH^@|q>+|A-dWi>@=EinRGl>F|0ZLneObXS**c`T-eKMAc7G#2{ zE(hT`(t0qGmdI_17k8Y@=ea%uQa^ZiN(JaHAcBMzgG4s4yw<=pFhhAxKWD zUv$k_SJ(4a-)N4v&KVtzkk6b9W|B6OjdaZAdgjCc-BbLE;%Z!lU_7=6wMu$cYaMD+ zp&4qMkIjB$X7(YSmb8jYu$4iyQu{T1mN=r|Nz-MyJz^ZoWouV)yHJ@NArYc)N2V`l zGZb!)H`f%YPbU)35Hzg6)$GF-2DrjsQBOD*CCvCz3OJ^zU}>iO0yqlLKOgPOX{pqY zj4SL(9h##?i5Kk#C-EH?%`(f7aCRIsG?2M6i{BnZMx?g8Qp<`Hrcp|ZBVx;nQ=Chd z6?gX}&QU-j*=B?r@T=3|Fupg}$EnOQK5O3Ac1dNK4VvV{qR6Pc__|*P>2nMxgPXz2 zU#;~6f(B8Suv2`YNBpiK4xAo2-Ozqezlv_~CQKIzZ(g51h_K@#nggCX{Se;pTYT7D zgC24o{Nk1{@Ps_|&-Y&^qVgvSq4xCB1t?4tJm-L|RIU3SeI+-kduJD% zZu?tlHakCUEEJF8h)JArs_qMvTrHo6wPA0h&pRJN8{o4YxsR93M-{7|EM|Ys_RgOg zN9vW_g^iXFL5^Hkz355q>3Y~Pb)7s(ttTl&T~ZtI)Wm%E?97@{fWGo;R}DK|AXrYJ zRfcr`|6=SN!y|dycHf!Uwr$(CZQC{{wr$&-I1}6HXkyz=CRv&1|2*&euD$m<_WICu zSM`_fuBy9_x_;+*UiT==&q-~dr5-uLvTDnqw(T}%vQC;`9g5P#&8atKRth!0@>-Q1 zHWDjk2`Z&p9%l|Uz@NyXr8liGmizIX=GuB6MsQt9Z`gx+5kRYvYM6Lu)uYugs~lPi z8Nj359CT#$uG1DjczduNLlYt#)bC%i>_p+BlJ=w0`1$oH_)J^M+4T8+Z|<|$Bg=8nGX}W0o`rfAAm{;maC2pMFobL1L zc&-PJJNQ$+iiaLtU%FAJX8dJ@ZAQK3I=frGNSATK&t~EE@un%~L>r(IxDM%SeHGIK zkiuU48J5Cek|6hIzLp(s#s2|P#pErW-F0VN4&Zj}zG?DSA#_nwIg1*SNB|TvZyS77 zxFXo#{BtO_+wQu$a?LzvTN6umI+w#PDV3=N0@Q>wT1tg#ijCAQJ*7+R%wep^Al6hm zSZZk+Tc?gbw$c;Mdgkm#>d$(Mv9(i2Mk0q1G*Xz2&hF&Tuu_oe#a+I6GC6i! z)76``c$XbNlxqf}lW)R(@Pz`&7?t1_E`sD~Pk8ZZv|zM&cZCF2aa@pbx$G(*RcSX( zQl1ZpGhtUy9QL2P#uTz%n4X2K_x-BgxG0UvezqhFM_&38SR$m8bVx2~I?Hdw+N3EZ zw{W2>wh&g0gi4TCF1HbvacNF>U#XAm6i7E7TdRwtNtNI3ZktKxVaHWG-}Dde9mMrX z$K8{!?qGKS``i@Yg8v z+o>@>!Aj3ex%{l&Wk;W@+&115z_(cJ=}z-5=kF-fa2mB zeak~aE#QYF9{X;2c%a&gp3t2Kx*=1dFlVbcC+jYz;**F9p(3)w&Iz}kmxc&b@AG9WD?dP zZXr|atkE%|Af)hwQ+z4O=5$Xo8+>Z&uRVlJ`~)B)}~gR=p= zw4ry3M-jE!u~@0(#6ZES?n&Ycahl6r?mg{Z?hSLAE4;ufmVdb_z0q{PVvg8H(V_`0 zOtfO-UF^-K;5xY?X|d2XNlA)f{miK zIcX7TijuoIxe!G;vX)XLFXvn(oC>8dj6{SJ92zX-SP)8arleR1Iss>f=N1^LaH7hu z4#z>dIZ>p>B}&V(gT7Ers3utJ#7s*Pk?KjFT9Zh!Mx)T-JAfqV6k)P{SFF*hwl+!9 z37SdrJWVFkG7-|3P1>;ee0D?3L=Fk99_X-MEWrdKJS9mF6$k>-xMt%OnGjY-&$T!89e5}qH!9oSWLXaUycs8M2kgiCl#lZ8DVCS?J zd8{sphAPTKyvBnkI1%UxAm*Ixg2>qHsSfgMTsb6#PBOtJNkp-U;ow;t+XhXq##0Ie zve3nw$pT*U9)I#|AxQox+?E==~V>--q;G<6BEX#5S_sIZew2^(_x+}3Z6#2aKxaMahQeo2h=k5VxUdukW~0 z0PINJzw#hgb`%IcGT?XB27s((o$n^jac$lXR67wze|LjpYEF3_*>Gliy#_ol10d;cNMW|_f8hqXHo^A&hL&{bb z=(6Qnx*25#*@Q%8=^0~$*jOb@G*lKQrmU;!;K)=OMUy!{2Pw@UAGDHK&}aF*Ut7z~ zo|#bEZA!EsbT&)R^;p2rf~EB`8%9>U>#8Hj-mMdSbX}%O1oZF!e3!g`A$x%hei?%c zX|IuM7c%UDHLR@=G>q+kw}ZH&4L1}jt*)(sbJ;z;Yy6*uoC@pT zLXLTWpf6bCpF-~a$3KOfK}%bqllhzBXE{ci!f+wJ67|D4-z4uI&<6$=1$E2BU118> z(3@cuckq$SQ2W`_hT_ea1Ds*k%alU7*I~9L(v8&_l@3K=x_*lWFDT?>q7o~p5qBIS zRQsj}r%mNv7d^G()V*_H$-5rKYihRyyWdw7OK9%Tca=I&R_q@I3jy@KLAUY?X=OBG zwhH^>1DgHFg0fYhLl04cRxP1_Zmw{2E8N^ys!m;Q-Yq?}Is|tFDMTs0Oxs~VQWs@+ zc9o~|CofJu(%+@nAGlC2v0(E3{-EsC6;-xY_2h(_;s-k_b}-oVh0#7c@2(c;PGqk+ zu8S$4uyC(KCM__i&=*)qmIJMW(+>$|u|$z8n9W_>hs}!9`}H~uC^LjYm%Gr&|2FM~ zr#D-%?u=N5jrn%!C~x+OOz)P>W(sN(?;dtmvr@gRml(2FTXU=RffWXZZh=Z#_)z zJ;iAO-;{a3JjdyONb?a`3oQj#Luw*7mR>ZGNs;!G1}=}0el7}Gm_#WCSZ&_(jsFv_8j@geC7()w*%ybXih=c_Y6u+SOHu5ZA<;{ zD7vuxa|hQEL|$urUavtHHbuDBTCvBL+}IZsb0~Ajh2$dg)P>|ytKSRA-I(u-1!+Gj zrZ-R3@AqF$iWb;M;^yV${pDt6{CwW8@(_3ly@c+Ak0Ixf?V5$Zc23l0k%4I?PPj(8 zGztRu#~tx1b`wCXMMy~ICt-iSp}dihG<1RSD)xsMa&j4Wp$5}zS-8isuhMQA{fr?Q zg~(V6klD&PzsA8DqRbU`RydlaFlNma4mz5ZK@B;Yr7<)*yD6XHI4%3RW#>hms~KCg zspjkxx~#kW*}q!r3Zw@0fJ%(eK_5iF;nKLKbkU+!SVpYt!}JWZ>6^2}%n{lN zzI2O1DP0#t+<8Uw{tH}7G2mU!Lekg1xj6I%s%77;>r{SZrRoMYy5}b#amlqCR4X<< z6p@a!lEXL{szk|XT1k$VahkN$cmV0Nc_+hhDkZH_@OVmyzEZFy5NbmT@FY@ldQr7F zC1RI9DVovAoAKt!l&*_W2KEY^JW7$ENTkmX9{L?QOQCfbEtu5$rj&$>ENxx0P= z>sf!stvnzk^*Dr=0@kLzpgiDd4qE$yKk$<~BYK+;IUzmSU=a%L6RM9*0w#M%pnNft zY433ax-+113!45sQb-yhKQZ>~ zAnJ0Y7&R^|lAoJ0k*tkial#)bl+DM8$mLXVdNX#OwVE+vY0M4?cu71+yL181U^*ce z#7&h!iF>qypAaDsPzkhNQ8uo2%UGL4;;s^O5hAi6g8h;XDy0b7E$@ z!f*MyPszf&xJ(rgiHZ6kFXN=I1lJ}VnvH37H9SiW$!wtU(FRUQ6_Zpb4cC%|b9c`% zc`g@{+wZ4m*(O11@*JIAv>mL}x{#Bq>Pau=fR}KS7PL9r)4P@_RW!;9lc{))#~|}K zSrMl&gkei#t4g_+SH@~mB$cn?Z?u1%`>OJx45K)k1HQuj zcJIUyr25MxDeXoZ%oPn5qON)_a)Jg5qI6CD&Onyz)>WNYMEJ zylGj;OAn!&@Lj|q;>Z!m8;L$NQjuCZye8rV>D;bXDQ`NFNF8e3mL~Jz@R$oNq((WO z*LaY3;8WJp@<8n?o6G+IhaX`sGdTRiy*NYN?*xsa=taZTEM&h6h8NfJhj}PsDBA`h zL>AM_8R1opmHv*3#)ZQoF`N~QUms`^Gl{`aj4VSywNPDnq^4NpvZN=9CHfU%B9E%* zn8c84qN#MJw1HwA1+X0EFZi;O(;)&5Bp8IwXw*{{&)7h03?-?JU_hCz#Zy{B5%IgC67@gDUxkDXH5PY)w6FRUgqMUR=VZHK-QerF+J&y zU0mp5RHrdtT*Cd8V}Y>RdH%H;tvSyRZ20XSTG+|I(|=ec6}&VcMdfSuGp3lzY>JI_ zsmx^x02|6^!&jw7%xmspMT%1vqs{u4JE~pEQ%%6C;KFYY!zTS6GvVAoXG|iEWI?fH zm+&0>1*JoMaL{;Qcu*2336yvW0&)-%aBY{G<^nOLGu0oI#14nyECyLvlF24ibLH%01gsXn zxxP!bp)gX43hj?)0^AN13xb>1j4*ZuB_--}Nu)-7VT4%%x$q0z zO?Dw^?8ehcAe|WzB#TvJk|P?>Ixrhl6pLD-n|VI0-d?RQ7f03YJqWy8 zJBDTL^R^wrXF=>+C5yIgYA<{nG8pQ6HlT*@x;-j5G3=(? zlK>X`nb3T0nVWfEtTQ+%{CTSt`ehacKT`n~y$-?js!h4CB0Ikcy7v2qjF%85O-?Ug zIsJp{cm+Gw3iE+1Emhi-gAcO6b>p4+s#Uu&#?X+yf5AQeP zr=F+dot|L$zjaBOPQIu%-OX)-7kiy&jKh>U+*dWs*huH6hVRge%O=t`gYWzAf`1o|ILG_H z3jWOq;q;5xf2mVdH+8%pHmR)SlfM!`0Vn=Idq-W0j)7_;Lu4xs?L0NL?|Gcv=Rk!D zwd#Wus@;*s%NKZYS+`Ps;4|{`Ll#;jPeY@f&b&rSl1+ZcDI0xO1}2q3i-8HUn!DBk z34cYWO^h0s`@Y+kkjbV~bphf-EsJ1eD5fZd4lXQ26@iJ{Ng|e}Ngl?d>O)3tq>^z@ zWeX5Jb=^}v+`vXHkV{TyooP_wncN=QO3EG>Q9A94(9mn_g?QWjgB(0y=<*SCVAuES z6gtk@aVsl)*Z47(fx+_Jd}!(|mk33*9>ZRQiQ<#4a?V!UvgPrO8Pzz4<(L)n16FwCrQ{Dvw{y@SD!Cm|JJI!em6N~bzkIT>J zTA!PnKb=WCVdDHPTVDpK#TUJ1yzWj|0c73JNp~IkO_m}8n1lG}qv0$HLp|2XLxCXF zY{_Q^b`RHh({LtfGa_{#OD2(G;m%@Va7iOOHor%toaSu#nhI1pcdjCxB|9MLX0BP~ z%9#l}e-FJuI3I=k3Bm8s!`tN$fHU|YkO`$7qgC3sg0=eut%H2c_it{E)RnN-ek=S` z!=%X#$6P27@JrzRC(_+>L@*B4pQ>)1l(HlMVw1aODEg0p z9dNSyvT)t(64W2W5~pC|y#fcCye5CNp#msbxI1`8OBeAub)~B3#1Q!NO4+Y3u4=39 z%MCm6;Yp5G0zH7{vl7Tpo{PKIGFgE;VhbWF*ffyC01PAifK-_pO!dq6S_!9FvdII; z6m3J!AHw&3_X(Pv2lPc%pv2g7jqc6YgKDRC(Awc~ zVoJOMqs4lHnj|B{8k`cH3Yn^p zRD+Tgd=1X0Ir5eLb!xIkz@(qI?JWoEMBiQv)lDrAym{V7#&G++VPu}J0ch5f*9be& z!MgPLi&YUKcnteI)dM=)^glB;-LtCiJ2K+pQ;4WV)gr51rZK6N&zKsO zOiD(j;ImVpywx=+i<%#+$;h%L^up6ETN4hrG`y#-T-r3MX0F`v(JOQGG0QF?OC>wxnjoF&gRQT8fKqd zj-2^n%Q2b-X2T96x6jG$_i?{Ba=o8b``O8D*W&N*Zr3Yo@9*YY zK~yk0pgjdC!Phs~&>cH0)4#ynhW}($YTt&ERoJNY-KP7+=SBo1P=9NYMc_yLi?PaLS&h&z?si2KHO}OFn zoSH_wo!Su=b0iIf=In-nU|3p?A2Xq1B8DsTsA}#&5str2@=lb<3e3C_JIoI!%9#26 z&g_cOeYg4jsxXlY4%rXaq6!2&0v-X6AJ^LiJKwQIQ?CI#f{Suggg%oxP~V@Vcaq`R zG-R2HU6d_vD->JBo(G^yRJcV&O8eqHnpj@W?MlaHE`3eqSSBQ8c?G|r={S#lL-Xy` z&$#+oIqC1$Dr{=kB4*lyYg(HkV*0f>QxS`rO5gbpH_#0Qlr0KOv`fhTdU%+(U-j@L z>=JhI>jdrXWbNNcCBij>);W?eCnP1&0@cS6^iy`bq&S&~A1Tg-skG80M!+S3YYsQg z8yD?Lcm+q`Cp>s+JxmFS+wM4$x*sw zam_j19|#axwZ8;K-|T#n(I0Z6m2oAV-VpbqZ1TieM#rUwMFREA75TWNM6FA z&h+>4FAs|TOm7R_;WYYt)d_Kg{(T!10RIqH1zF(Ij6p?Qhb+Mi;*VGyQ^}fz8C%aN z57{K<5TP`8eOnF;pxf0@SoOY4>~H#oHcS-(AW9y*Z6gV=uz0gLPTROhMV2idF~gM(;>jP;sIPDu@VU(7PPv&yK+rAPhs>!a`X_DnJw<2o`IDsUD(pY9sWLRb*^Gx6*#hPr%}<@11zBGTNT~g++$Y56*s^lvo3zGY;A>snc{Gg5>tfb^ z2T6d~MCc;)5WWsgP=}a7$lcKN0830)XyoES=F{qTiDq%6RGtmgltqe)be3D?%h4Z! zC}Lr=q%WA`2tNe5)tF%DW$6RKHK4OX(h z&+J{qv(aT0_w!f9w{Bi{HN-cMk!7*t*9xpn%FUvyUvFn(F~a-SL`Hf+-W%F4{>EQy zAaCY&w=lFF?`c0C&te0ge3<-BWw@?Xsm2ce%g-T|w7xifU(8vI_(APt(uhPe9Xksc zp!4ow(lldKmaP*K(`(y~8Q|Ce0q0BIH_7&T*Ee_nB*d`EaR%e`(vaKoL-uwj;w>-QjnlDnOx36Zva5%i`5;aHbm=P|>|svc*i5}jExB(|#!J5B5Np7;Ls zj?K~v;9G9ie$42mW?XOc6V0x(4Npr@+anY`3qX@$9i}1Mi}@>u7rg;w&P+z@k!#7# z24G72>&%g(Yp3|}p9#LvSUr4sA*ngsV4z)Ymh~zAh~E_6j{kR2v7;!T_}PPlBVC)N zgX;h1rJ_dSPwRd48i25$L*HzmbA z-=A%S>17_-?<@}6^6*VkKE2Bff~&HLR4;&0MV;Eot-b?n1aSM!U-DvWuQTDdL9^aE z>iS2iHYX4?gK6YnBhP4}DJwF|e;Ikew?0|hC*>Z@xG+f+#1G1r&$y_1D~4QXsr)a< zyPa0ybKE*oA$~5K-JVOWA3+giD~V&E_G{FpsHkvXn2-pHPxv5N!6NEWYtc?Bx$^Kj z7b>xBhEK9Y-GME$*Y$YA(kir{d*!H6KYS?3{&Xd`8^>OFSUGd)+`C>ZdQnob%kG$A z4=rUmUL5D!Jz$<<6kIUx?QV@-=iAmZ^IaFf%{f=X)pR3YOz7c1m2i$dwa{nlrBh^) zAF=BmW@qr(Qls8JIILYwlrYZl?hp))2&A|IlAMJ#4VujYjaMsj0yS(+sl4J?bU|h; zrT$tGdMJ-Bszy%{O^;mw$#jRRlcmi23%e&TCnf*O(^21{NO{?y)a7@dYHEEjwPtXy z_NPa*=IoIci74n=kP;UMV!3_kG6@M~<6E%IRU!H(shgfGon99FpcWmfy?xl;J%Z+T zeOEJkD}t}9ub^9xuprpPUJioE=_UkICxHnR=qlNUDz*~S16}Di=8XnBsRfd^6IG?( zOJv66J7-k!rgq7RpDYr8P(|<2oo)XE>a}zr8w*5KxHrw8`X9h!(sDEoo$hWRE zM0>{NshVea>NCfLx9dvNc7jc({`0OLFW%odUZAWZ3@f+!i`J;t5q+r`HeZ{w(cQPC zpP&HZN^zCAay)o?1l6wwmyN@A3UtU#N+Tpfw~M0W#%^N{xk!`nJa$sNP*$=Vt5YV~^w%54Y7g4JLJaQs~B`Ru9QGVOYwFfmnC-ETy|x ztYlU)i|OE%jHrLde@JNdV!A;Tl9AeSdIG~?kaV;KmViUzwksPWX_XdOeHo$dADmqk%cPTx_dm-)4b8~t?B6t zzHK4LRQ;@Tyqf-wMUm)ucC;ukQjy=WH__s4!L8uFAMESiC=5#zEJ}VZXlXDt3)z_`r%}qQ zI_zkMG}gdnD07xBQyKl62G$xwV+!Jdi%L{x(Z)Z!9=rUt+o9hNO^Z22sH8jv&9JN_in&H(U8p(?_e}*$|1{5ivah(Z z2hZ)j!>z;{0P*^OlYXEuR(Qxig6H)I?|@V?_95pQRq!eZg26RO!h*G#Yr!#*Yd@}d zROG_6BpCCwXIAf{23z`CDnTqsY7{F5jafdkLJA8BuSSbntf|*HWBAGbYVSg_x$}&} zm^&%Hl#=7nikAiwGG!o1OhKqHSOhWx35k?MN+LPlU04XKUUWGzUPQs75XE(g=j<10 ziljKkaj;d!{^9MhgzZ`DizFfQBv$}uS&VVIxVf^Y%ZdyhZo3d3u3Hcf*R9^s9OqUJ z^8yL}ilVpc7=BDt49*S<=~)Z&YemtIO%V(MBJdbB`Fh4yn%yTu59Z zOvsENB>p&v_)btic*smZ$2LvJwgPe?nFu;g3hqY(I#6tmZHf+ohxKkvQHNKgWKSYr@y|ABEsI_XOCu zWD;eB)ouM^@$SFm0hOd0&AQ}iGLF)WYbDMa zQ>l5I$(T}_y5Fq9JLinR;jXDX zMoQD)mxNKW*B?7Qp8)x>0YRuu#5A!aB2;j8F~tdyRp-@hQdix)XK8Jdr_!+&>FpGO zlu|BYf5Q@TLKjm;nI9>N%DuIsEjgrA?`B9)vje2tcxodxq-E@p1K!M~$ElGNgD^dT zs%pckx*QTPPD`QQu0KLI$ubtKlk>+d>h&IWa&7|TbTL0`woDy1^<@CI$I?!qCih&W z#!00rtVAtFB#xpI@LRD;5qmfVc8n7N>^^?+v_nUySx-ZTIKjC zwUTUd>kS$!kR8BdyoVO6g07-G+n&&$-6ttE$TbdHwD!v%q%Y!H6UngKFjfTFmq81A z!dGc+jl``GFT+-=hO~QgClDXqdZemGu?Y7l*S6z$)9Ye|>jy9(O|qzxdW!?DmC0KR z7d=i>ndL=~GUXq7OeRO&+nsjLsq`pnl^3fPy_#=2u10NiVDNj_x%t8&o;{~oj5e;- zm&?H!*ygXVEME}u&;Qx<;ODELfBP@ET#;t}-AM zB6gUT74P_eWq$H8__z6~q^%HX>Hk3xooD`k2x4@*9_IKWJ&sFFq3)DU2sCIi^ErFe zh=J2rZ-E4g%e{IPOV?G!u^ zRQ^9QEK1E%d0sih9+!RJPM75oKe#HbI!}LFYCO(xH&Ezev~CZey>s52&(@mCU2xbD zQ$S_nUIvd@VpOInw2-KLzYI#-&6mv_LZ)asc6${*E<*3sZ`(Pk5A+HmN-eu++zLZy zK5y9=JPQ^3;r?EEq!c3he?*7|b6B*V{{tbKa{Rv#B0y{P3n6Cn{(}%{+-G-)C3%-8 z#kv|n652Mu;wzzE{}o>$24YG)6o;n!G0ohu&3;@spmvJyKC^l}puVT?{l4m9BZpB% z%w4ze2|~8!xpz0e5!{nJv2Z&6nZewow~aIY5Ghbrss6pA%$vtX%=;S9;$)IC2R^Z^!X)oqE#Q35dM8bZ z5M+H(`50lI&_$9UdFe!uVxC8Wp3R(i>}oO1QR>wp_-2la;c&4aUJKuPvj=TNbT|^f z3`A&5Qx#PMPpD|t%`WIUet4U2F299FT&XCV`7uCsoUT$x};!jM%;>Vx@ z;w`!3$FKt9AqkO%N_~IFc0&<#^tsu92tfoRKoBAb5d;fB1RwwvOnoCnunVqDaOmPn zqw=0AtZ02yf{m$in#@Kz(v3BW`w+yg}eRb`RL5y3@RLxL)N3kjq{CYsg4 zWBF`+sX(PjEWnK~xXVc(+(4O-TkbGrD!zopWD;^D5pHue{c4mU<6RlOH%S5{-DaTk z_nE{+JT;V6gJIkP?rNSIRio`@DFtIM!rvnBcY7*Y{yH$EAi}v z_ClM%U62FQ@tB0n{q;>}OvK-l;@XlX!>OdL_NI-RX_$7yqj)Bw9nD80FSQ0U_*V&$ z8`H0?xMfBeq%fHXO@!QoAa!vT-_}(B`02ep%M%Qx65xVcOYZGz1pV8nD~06$k55;U zW7~`)g+y!vMkVDx?%YDuN8+~elEoi##JbZeCXF*eX2vjR^%q@_`dnNVIGR$QJ?q)wXTCi%GZH%2WkNt@ z_=Adt*o5&m;5L%mx@Copu46wAW(=0c1AyTUBaTWe1L=%VPa7ff{z;!YRS`jJOK>$4 zB0X}M#@lpzlG5rBadj&~Z!u)V1BRtd!{r98M_jH26)<#GsY#n=VW&$RWqxVMLia{cW7ccS#U6#DE*@^&jN$au^(scN@wi{Jn4b_fw-&Aq}S zwZC(EEAnWzS1SdM^Q>>)t(q1F#7$8q{n__t0vvXWQL_q=PE$w(Yokz~_Z zL%q5jYtBixu}xASe+EmFINMwR=kw==Qs|Fqhj^@=>NU=GT;ZBc3GKaB=>c14F>IE@ z)%xXV2H$sPUz~2Z`t|9f$%VVu>F3K+v*yjmc`t8P)9#dk&yQBpvlx~TwYHYsr5I5F z$Zr`+7yG>>sCBJ|N^caE$tj%TBIS%}u%nu(4k1XhPLViLFPb@HPjjABF~+f`(mBQ9 zZu#Sef4vRw6$J9#P@F;+_o|&DTUtgVJ;}mgO*^mfC#AG`BBEU!Pii)jGVo&K(t1=j zq#@OSG0nY(pX+C%F^!A-w5j%SEIMX(YPDnJK&Zru5I8zs1(3ebszx$6HILN6j>zUaxf~%$1UzaGH-s0ttBm$rS zWFT@7If!hARXQQ!U@>>L)Je4l6R#NMG);iRCdiCrD&#p=DY4-?$1v%RRa|~TN-G>z zUlw_)qPUC7=BmS#aN5uYuS@&u&IeowqHxqLK2r?#OhM$*e zKM;T$LIsWwcgz5GjYSc&ji*Vy{@EY*T@P{mJ!G=pEPT6XKD6`TQ<#Z2>mJCY z>FuGbI2QGR%#iqpyhOf>5^BP)8m(l96fHG!>dTn!F}t;jG68DrytWLWYyNKT{b@E@ zJi30|FHS8?Fv|Ic8d#wvHBe3ID+m)E0NIpVvn;9#%PyY*|3~wg7HmHMm!mk6PnP!` z?T6e*{8;@<7x@wqi_lq^0!$%h7)RmjknOX}&^AegfZ&=2dM1&>LbGwvN|D82FicKT z9*-kQ2A90~5==@L*q1$G{(+jI4o%@x%7x~VK@8^%Xn+FaT4+2ayADGmV$3arma&Pl zbYBXo=?*0`Knmb-H^5%E$@6)0p=Oa%LgtBI^KZhNZu}p@TbbOn2D(%2)c4Q?rrk@k?}?a)LXK7F!y zW9Cyr__R@!Zar1(jDH$kr zyh20Aq)w|&H6im^5OH~{E0!W+YPEVnVk;vQpS_Fk-D&6YEC_1osjF7a;+r(9`jfpX z)i7T9euD~6COzRnjpXPFVWWVMUM5^ojU2NqQv;&a<8RmEotBWfpXA)gV%1};oyh&8 zo7jdXA8`wFu)2|I{+M|i9Y8A;u1h=z8ct|y3bW2A%JZNptn7_$!9?ZV#OO)3 zt!}lCS)g=8KxrPTXm@LMYjf_A&DHG%j%0hcDu1LMr3_>{evb)R-!98UICrS;XbZpj z*ljg2*^E6pdWSnMVa+l=I*^zEWlAzGk(>ZdO}t7tDiO+r^v|(&a>5r`Y#gRL&DT_r zG^cKh$ObzMF(n{1tOW>(_EKU(A|q9Bb1Y+izhcQGC}LT!D;ATz= zK*5cGmiI=p|vhhMbHODKq zl7zw@x@Jch6zXHcFaX5?M+QciaXc5)0zNJRUy@&&M1$4Hj}_(4!^{<3$?N83B$9NZ z;>A(@c2f9~rDxM^kE)~OPiq7EKu1R1Q$gID+!9X?SKAFrnKWAh9Nw5xrBX~e=d-FN zumdzXVWb%-;_7pHW+y5l4&=#ZFdrmJ=FUveS><``A(}^U7RV^9=ZagML73bFw@M4 zDB7~5`q607m96 zqk=Ezzc#g~Ri$N!z8LNP9OxNl8XHm?FTCK2%+Rn>i-lg9SlcZp@=^MY5HjtYxw~>+ z4#{Ngc2ib6jISGDWy>_Z=vUGMoGg_=w^Fqs zZ;MvANrf^d*WAbR`ByUZeigT8MlOHmvghr*6rAhzT!FMmswZGZF8p+*pVh~0Vrf^l z!eq!q*Mu6zPcD6yk1|!DyzZcBfQ7<2$4y$-?YH|1uNgPbvMzN5TxZZY%6PJ17%tcO ztYO#)df}k3IoUW%vZeb#hU6Emo5-M~!N^iGvrh?$AR!-V+)vS-JEZa5)TotG_4X>* zqluKTeApwKU}Y!B&346e4J|VV=J7RR`{fh@44bAGjW>PzM>fZH1^YwXPTbgO4F!oe z8@!0I-AZYiVxS53X?|d=ed4RZXwu9-7!E24(+A|!j)NW)cK#YeHgV}HF3kDwWyO<& zVc!5IUw6?LA;&QTh-|GzTNI&0a|;cf7TT^-)l<$5L)6GtSq7p_I!YD))YRz7XwD47 z^!Fm3(Q2WhO*L=YvKuNREA_GZl&f2HyN8>TElqn~rt~*Sb70TG^rkR(CVr6dKeoKXHr0YuEBNsaCEDBCy?YTJ^q2>8VH-OmO6*6pV{ zz6Tj(T|I}FV&*g*ngr;r{-c!ZUBdgoI{GVhY4M38$Qus5*e1wMJGV;Qnq~Bfhc6_q zowbl^T+%+Ax|gP(uAO9!F|7|{qHRr}9yeE>`Kd*AE5S;~o^Zr_40PPPnUgmB@NQps zah|T#w9_D-5=6#}{g{c!b;`dmGAC^bra@o^o8T$^`t?|UNb9BXSMoLpU55iO;_5oWp=S+QbR`_I8>1&Lqh~%V-T* zGF4%t0-0S|);OolKyGmbSxg!QmGsHK6c&mM1OWCy3?UahlL&DJ>r9e)+=sOA@19H> z6->`BXCW|tI_Hz^Ygnw)IK1|H8ZP$T%w?|Mrn60_lP6aHkQMlmr^FfO;Sj^)n>}w0 z$UckP`KK-a?ch;Uz#Q^}qjy!TVeaM5Jm_vo3>a(-%Ddmsvg_36n>5XT<#OjHv>KLe z$k6H!e5_=r9NHH!NiW=xw+jFPa{r66w~UIbd$&DLO+O?=reA#=gxt=wD^U?G*bbIW*qH)^@ z%KwOon#zGFEm`U%M+8hwMy)<-H51jK{`6nf)PUyv%kqc+10{X)Kcl1(#x_$d;%R?^ zgR~9m${+sQkhxR%A46tpUjL)hhT62d!3T$L>nE6cXWb(QykAr7knixVkR|VsRN4vV zY~Rr9B6U5@%{!M}MreITS?}>hs2iU)!+*Q>(loROQTXe}G&&?= z6aL?dsf7RS$AtBq68Vh2VpX8~gM?BpRXj8(#KzzH%L}p<&mqR1nj3r|OV-sXE1iOT zW!cZba{Tu4@BF=~6l7SB?b})mO$$Swf<=?`Z@)zI|E;U8iE}K+{BQ2-7pVJsJg#T~ z>b|BuYEu;uV!1))Dup$fPkQCH(CRpcSO2bkji7zQ9Bt6(E$oM(jVj{7;L!5Hmq38O zpYh$1*UP3R^%)J!$9=@L#Qx4?QjZ3tj|LXf1O&d-KJPo&>lc0&F2L@Pnb}C3(VsH4 z;n2gyXnh#5;sMhu!~WoC?9-3qpo>;cGAqtN1h7d zNV&khJ=m7MP({B^aV2=Miu}~OqeH*bz1{MF-uuBq&r-B zt1sr7=dvRvF+>3FV)laiOPViHz@+PN(gJK4YS@qbcfjnFc<`no->sT|9RrC;g~(L@)A{D1A}8$1V0-XDAZ6Dy}VGE`~Qd}@WYrUC8h?Fmk? zZKLR%Y-UnBR0OOy;4YqUi}UDNgq6FM_pW-BI&4$-fFZ%iXlG*sy@l`jXAnO@q89~L zJi>CZLEDC3297Awa>e3st?v$oAmag26EX%=>G>ml=(DLPh{e1yq*#vI)O*TR8QFZP zqN^gybisA-OgmbPKrfUG8`1)VR!xe!Xdelxvq>p` z|H7Grxw7K9w{!m!Nw9T;WGbpKbY2Yk>L}<>kKvcBpB^aA+U(M+uEleIqZ>;9iEfC1 zMmMB~Mp7`^OWs1BkRPxw3*dI83HpAK43E}hz?zetWnBoTi#ZJi^y(gb+E8s(%ra3- zYq(K02ccQlsR^5d2+S#}_Wnj9WWEc$$~MZq=+&klaMaXN|0hk6m_FnNNTYjGEbmiy ziSbiPKFr%K%3H}T!kgp3R=#P_K_T6bd*y2WLVL(P_Ka#>z@ekSC%|fR=*l_AzHDWW#i_ZaoSLM?fK_XW$-r^><-%1stSrLk=e|kqvvQEr+rsl;cAmAi!|?IhSHDj!wvL$Y z$%QOs@g5QJW_bj?X+?xX9Y~EqDgm6~#C1v5pLs(g@i%iP8cmW^mbe?CF@g@C&J_dP z7?|y*dgJx(=54iS3{#fUdA<9F4Edg8a3Y7FJkwZ*_tmFfA}dlne!Q&^@QO+pDXOFR zW*|nex;(udql83Qn8RB@UJE-7JigKaNti>LLAx>ovD~NnvhGN}ficNMPHReJ?$j1r z)!#KqI0)q-!{p6)%11>d<4dm-C#UdaX`$b6 z*^eK`Dv2Kt9Z>GHMMBl&&Yo&IJ8Q@T1Zlz@OGjB{^8v6zhWGCLdyQS5xz`=r!#t1C z#yPdj~1=) znK&@}Zf{ER<7qb91U8_5o!?#G9Zi&u;?Lcg4Z)=ZJ&3w2m0iq0dy@~~IDc|$Xzk}c zvvX_EqsmO(5{arX4v?MXkinS7$uD|@Bp1?aR758`;RqA`<}L`IIYvR5`liK6hyGK^ zj`@&S!LlXvpn^ zG=uVI7ClLA6}rPbdJAu4>`n&^B^ov(%~GTZpfM!9(gbUXo7MlrsY5SUb`UA0<2J3G zPhV6FQq6^V>YrDpaxe|RE*t-*vObBhK@557uj%*h|Bqy zhQy&x9hO}u26L58klNd7BGgxzbwwktUOgHpeQrS_NY=;@akSH^PfOr%#MwMVJx@Hr z%O-)8+;pj%{oo?&nQcW;E*epj#;=oEbWQ_g(+CH1AHUBp;o_MewSUfe`&FBeQaL6x zC;Mr|)8Fy3@G;0zR6x=v;vVmUX=_$Uo!`!>UHYGohk4uT&K4JCkh>;pIMh*fE%*?G zQaE1Tj^Ya|E4>?=tzwhTro*wy$v3lU-1IVs$Ep&AVKOPSGhYoBl7pBqu&v0$5^++I z0C6-ltAMx;;_-|lz|0Bl&yha_G7_BFO}7#oyNO!NA4u#Ft+~lW|ICf2o!}=#aYDS~ z65E(IC6|KxP{Z+Y%Him=INqw(2>zHc+&9X7Ox%PSqrf01ikFk(9{0i1W07WX)f5mk zHUn@vGiT+|jAoe8)e2=L`6QccMn}vP%+=_21@=1I*jlp6DjA++%nFRK_@SvMVG+=s z)CguxIh8QVu2UwOqT8uET&d(vo!%Y=@Xz0Wda)bxmSu1`xzJ@1F06;E{JvG;=lA;D zuw8NORQc4A>B@~B#;icB6p}TI$N3#~`y0pKm;ZDlw^DqrC9#6tl!8v-JOce;p9V}5 z5;a9yc7%TfIG=D+^PQ>NCQ?;=3(1bqb);cZ=;o-e(Z?rsovfAZw{9*p@uV;7qAIf~ zVLh0C$UjH4v`o=FuFwh#u?C5AY!d$q22>JJ!m1scyI9q29u99*7(qYj6a&n*`tZ}1OEDkj*F|p19DAqP8pGFs-#)S-RE%-fH zI+95X6wR2V>z(}RJg%jVW8&~2vRq znAjX5ks6_MijA>VSTmrO3kd^$D4x@MhyV9Sd-Fg!IcKqmI)xzKX{u`YAF$Ye)noY# z6;W!r!Il9kRW?#ihJEV^%~=0i&Hz3;tn_C?%O`%B-RpHdSj9$NJ|DVMm-e!8{{3Cr zG^m!CJ#ml7bPxLhG-2)g5cUJ`$ykXrpHV{9ok(&KM{;pq6_1YDnW2aw8#tI2JqK+X zeWtM?a;$EIW0A!4S;5l2ESKX(FoWk~SUw^{`)P^!@0*r(R7Scp`b1raD_kGzZh?=g z^ZDB{mDYJ$o)Nmu*I&Pd>SC|r$kEofe6~h#XBM!Cr>zHD2hUkcTKD(C#M0&4Ydxi= z?;W{65&B<(TQ|S{{J0kTbag&>HZRTWC+yWei*X3}d!Rl9`N#P{4^eO@wQSnG6g|4<5=K<#9YRm>Wi-Y+q*>0!EeQD}aQQU~hIO&Lm2zW4Y>EDNk8GrF$RY zLYFn-j}J3X1u`kW^W+DG3gxwk3*~R7=nCT4sSUs_E*r#7<>$b$Sn=OMb4X>A0j5K; z@#=U>@rA*SiN+yhIJ7{iP{l~GU)bNY7^Ikq(cYJ*B@Z8V1u9JweJ0!H5Rz+AzKKWP z*L>`8WYX7KkxZQm+eoDjC3ZrR2%2z{3Q~}#!uS@Bh+;@jOM?N1-W>`%}Oi*`cs3%OlBoqPC*>;iggwe z2?>0Rc)8h3p9aun*~33~&ifJ&^ayc?`72K;1&Jx-zivV~MkMqLH*6{T?rF+ryZs#P z#ZRK0uYSvmpIFYb?sh9OjKzeHAa;;O-_y@I=t3puxv_ZEOkg!gA#!d1zk5P;y3^ez-tk5peLSk!R?*JwmrqSj&6;u$mzW3ErKjD22 zUfxhP9Ttp+7x)7SwsQJQe1k#wB~&Ee5HNgy8K}}M<3Jy3uz0*4Yf;o+`sXE&+S_6( z_QM8P{2v_Xe_=qYFg&J`;UV|GElZEz-FGGoBcF0(hs&ZkCv2QcBfG^+0qC?e^Qcd_ z#&rdV2vse?d=lbYr}=r8p^wrP@o3`$7O<$}LeUbX6f`bv;xv$Ma6LA4FEI`^5G(_U zMK-Pm{O6Jy8%zU{u!N)GK_$>FsB=-7s^nxDZ~l?(F~P;Ji&$hCK~v&O1JORS^ub5t~-dw0ux&`q7Sq?g# z30IRyZ*sbeLIuuQj#|!Y*gGo*E^8eGw!jPQ+G_>VoM2oM78GN;Jm~Df&{_zJKWq42 z*TT;;q!QQ-D*qBPNL_f<4fe#UWD)}^BE=~9IZZ$H2CLsUEsC-%SzQ{PuR;ayx{pvt zAAdEN6%K$8*x@O%KpaUiV@CbG*HV!uqbAjFv!xHSB}K+v?mUJhZ?|{i?2eoDAO_)JX0gSnI&d?Z2w0WR;sCtyN$TZ&zy-fes2YHUe2msmbbCC z8VCIc)FVHNdjy}OZsKckiN6Vcg9*h-DOl+{Jyi#G|B40Tipiv!S-^GN0I zu6;SOjVmpsTGJt|Dbf8SQ?nbvJ!wfGtka>4aDh>^L>;#zLpH1CVI4e%$dYLsTnm>E zvJff+X{i^2T&Ji&GHEB_(B<==qRHR6kQzE;))Ddy$;K*Ecd@0Sn+HhzJ8v4#QUTnk z-f5M9bXv%olBjK%-|_3nmqlton7`*QmE_Vi2^oNbD)=UJr4uJWq^EJvpAsY^acOC- z<7t8$k42mZ2c>GICWri8tV0CMw3*EQnHrhQ{-38trqftF6q^4vJ*p4=ogSOZ>p}yd zbYd_oy?ueH;Wtag+yIW^%98AK#w=dD7|R&5Bqq~rD3G+3BnW?kv0e>{YiiZ`w2O5Y z<6$=PE7O_&vVV_Uno0u;Ah<>is6mEU!-Dl4{?$rh#@f zC*!0-R?v`6D}cfaC)6up!ssx*0g zcleyKf7WqC?Ev0@bfNsmBm#K1isP`A&l<_L5N4gPN@^}%H-cneuVzSuVXg|>ydAzs zy8%osg;mm7h7STtB?VmyS7;DD`d!km3uNge3Ur@norSShnpZ|PbVIm{YH;$(GR0*5UR0XE<3P23My7;M=5R+un~ z(AWv2@f>}D)BT&6SM(ZN6k4b1WFGmnsiTH{gKeLKWl6@}3rXDKV*j|JvBQP!LLldF z^PTc{L3#o$mIRw`SF6b<=F1)D&G4zVgyy^ueV;`>vT`?|m|#JOz2u&T$7x#WuIk*;jZ)q@UsD0<_M@oY$gE;C zn!bo)>e^(vs;wnwb}q+fzS2!>)}VmVp`+kT`Aomq{j z2`V;Ck6>@B8TCjLN3EA{@q}n=ieVaZ%UX_Q%TV9THG_Ey@fkL32}WxV z1zpn&$66PNG;$0k83plFkdvzi5cWgt7&ALK7tCli`dm`lAmaIZOhl6}WMPrnqfk}} z=!F#0j>EHQe%Pe*CyHq5qEIX2py_DK-uZlX(SR?u&o=i@7nZ^VbOXMJj6D@VOBIfc zj18cnk`Qx8g3&pPY#3-6OHzXWcL*u!%MRm%O_Xl0Fc3AtkZMXNnmff7FV2)j=9W{s zRe9I6VgVmnCP^~cz$rF3A-sR$|igq z1`VkMAB*JrT=RND&vSMx8Z{x99iori3~-{}0Y6R#&oPQwV6dCEyJ?_M6De3kN*9s2 zMsQhsIDli{Z-Ey`bOM6woEC@i1cDsJiX>Pe%kNjMMEcx@LRl~KTJB>LaUKAd)H~us zp|M!hlwc8v5AuHo_%_7d6h-=?ri9^fgeQAE2Jl7&Af>X=YfSChj{&5r^>C@Of%3#m z3`(Re9!MPU64EBT!ocKSC^%l~$1}HT(2z1PmA5nnz?Y+i(&G?I%YNIpa!>~{#^5!^ z-R4B|!{|Og;b!aL{)?Xb-072}mwBi1*}Aa*ok3NMQ1t&XU)dqOJLIAYuc+N7+KoHf z2*L-&;$;pe>*S1znsGfQmYSD-`$nXp#+4*t#H7<(_R(taM|v0TvhsNyj+-uO7C>M= zCi7mS`7q(y=3CWD9q0Uz)zSx(e<;~+M4#1%5;LHbY~(+bEbBz&h2eK*EP@gI4;ATW z&}7Kl)vx?#{9tWsMHjO&*)?gK_92c@94IDh`X3Y3e_^uh9;a%tGY~vB#eidY1J1=j z%~=q{rN6xmEHA?(*xpD8huN`_ARfek~ZC+$x!5p;64uL;==D)s?P4EwB!4L$~ zuErNj-bUz*cW55G`w`;uE#KLt5qU*`mTQ z;v{JMe0@A$J9w7;nZDxny%nrtl)2y`S*hB}Y=cJ9)>?92ZSvOIy;4=y?We{BzL{m~ zZPfMwfXTwtGQB)@kh382DLNhJK9Kt&P0h;5P~2WS!b~Ztl)1`=s8*1X&3Ve)Nv*|; zO0>vvu>DQO?~)(*H$AQ<-cBuxE!NTPrwN9hRs^!SyT8jzmbH+&Ome$$k>j8dwR z`S(&fSmy1YZ5mQ;RG1>@o)k2T)VpovGFeVH^CFs)lqfrWQCd7y*p1umNbAnm+1HcF zZA=#765p;NI68%g||t`^mh$aoU5YRja`6c0(#bbW2A*EQ?LRTp8Q)WVf5 zsKijAF-VbdJx2P`EM_8vE7M{IY{!KiUZV^E3P=XHEMF9*#6@e`g<*Y+xC)I*b!xp1 zvIV-l{QgNg_^sm^ovYBfPp@S&!fw$W8qsg^$n?qf*{`nMG04g#$k>PB8;^#SyGGHX z;uX{1puei`Gx6#=%;Ui>b5piix2(g!g1zcJhi2X+DPZNd2?|RNP#VC;g!+5w477UK z=n9lN1(%QW6P6?{C7#rd{yx6hXJoHk;L}dndcfR%kHTF_k0r0D+pnUSpDw}cnQN`~ z_dWcdEWY94E*U75I6{Dzh%?}VI%Pv``Z)?;Ll()4OK)PUVGdXe(N1>SORrVn3ZFpT z`+5FwbEf}&V*u>bAiC%As3JTOHn>gRLX!qyq`;CBR{ODuc*aPfDxB}p7b%Pv^Y7(~ zkT&pFzD@YxPHlkz7a=PH%@S3Z(SW?TMlx`zGAVsJpV^3~OZjW5`0d#8*sk@KqrL*) zO|){0a!W531KeC3H>Rws%e?&P*E)7#V&1({$T21_J?mvEmWK*Sf@8$yckeVTBCemq zB^B{Xi}85DVK8InWl7cDoYkcT&a${=@a}2Hs-Sc>9&)r9GCvSWWMBJTbMow4!9Cr4 z7pX&^1Ortg8tTL<_Nwi-aOUHLE8z2)$wwPs^Ka2Is|J*pJ( zZ+1UmyJEmt$G9_z74>7#Vaa_VDdHTtEmmzPXn)G(MIhRmC3x9kAmm7c{6B4XXE@fZ zH=WXFJZI~_#w=?NrEwZZxq*>$G;!h(O2DXrj3qes>6sELoJ`fxt|~q;c$W(qhU9hn zOM=p~eB&Z0pM&WVR2bH`v&4%H-RFi&6I2=2jf528*T@nYLZ(2 z9&!Robmj0^Hy9>5Q@YQ!eNRdWV-X~N@_FK8HT2*1wX->j9YYRpHNk>!uN{de4w@7Fe0pYPOv7ya_N^jwq7Hpq;j*omOE7BZZ5Py?Qx^dJ)e-1s~B#I4aR8_`m`Y(2i3(^A11Wz5SCnhf!vL zt{X+!UE>gi!*Xx@YW&G3n1=5UbwHWswQ)Al$BoInAe$m3#|6t0yY202UlGWoLKOGU zv)0h4n6lM3a247Rnj2C~DYXLi2`&sm5{E3~&Up{ihWxi9=I9Yhj5I_BQ8E2pMN*U% z33E*TcRs&fkjs6m<~>eA&fBF(dWs(5EDcF@`QCggLPPr4b0b>Vd0NByJ4lKhUlqn3 zo{f#c|EE2=TI>7c82|mvdh;ACZ?g>cGf@mtJiDp$TnLSR4finwQNKQCVnTf}*=~|> zyNYKiSh@XFyy>j_&C)->p0#xe*{Icr>&ccMs!bfWy`i7)@A}%sI|&3_x0+%$)TU0( z6;owG@_dlZl)<x8=x9+i;>3?Bwl zP@)~wt~1VKY|rthZ@Agd&c0e(JC z<)r6PkYPNw7r5~^-G(_v^4#7I_PbLGJ9TT2le#mqj8E0 zC;o`ezbicJ#~O@Scelhn_x#7xblwJF?0QL>syCFw2<|>vt|a|r_c^biH~3EVbt;hP zVVbXiy9o6-)X-7n14fSmMywoiE!(mm8#1?0U{zHTEn0-ghmPWSPr*&C?cI9d$SKg! zuj05wsLp>TnDF~wFI^D%<8{1K*aK)U2^gwo9eL zUJ;+`RN5h-B}~@CH!J3?ovPj_653M+j)^%RBzGa}2`fheC6QNUl2#_;nj8c6etjeU zm#vP%&Pl>xe&h!uA&iHvZ!1Xnmmm}~0&$V*>ZQ3N=u0I$Yn#tqu~lT9q8C;H0Z@v~ z$hF>!25V#HgvV0Y?-2(M_0(vZ2hMFz_mO{|Kqa?ys}LMoGh``~pJ|zj#xvn<)4?Jp zf+Krns43ApcsI z5w#AzCH`z2haK5=yu_B1L_xUv8VThJ_GDx=ZD zrEjYT*s8-Q7p8@%{T( zs18`3>6c5BL&{Q(mZou@;Tx3D8f7PSnOvU;CuqZ^Q?lDBc+{M9*vo7x@0aHh;30^OdTY>=L)>!Y;N`I;R$YXHMrwW7DON1u0OZmd! z>A7e~^`pD%qrldJ=SFl!-;H7Fso9cb>js<9f~K8<;0EG1nH@B2`QOTU9$BAEwD2xM z-TsBvD4(Y)Jyon{IG{}KX@TCX*GVZ3n1dsUI7Uw3w^31E2oeLlm7sNVB)o`*8YuK6 zM$Z{dQMz%jyOp;Loaep1nhupeExfA00-26(yIWKRTPS@>;|VpR*FTdYt7>j`467&A zC>t_RoOFkqM%azJ1~V?_0W`_bG$>L5ngn+OVvY1Iy!M%pu==NI*U)e9nzB*fg%EBR zbcGUd2_UFaR)}xgzk+loh(nc2wLy;UY??F(jn>F#WWz_Kdv7VOu5+1l4Xj^Ck`946 z_GQKjj!qKm$KRH3YyCit3vAfHjH)#oP_gR07O#3osWEAi@^+O5MQpEKl!XYzT%5MM z3%%f-&Z-P!Bcw>eiy)yv_#G2?W=uDQdzh<`HxhC&!fiuSNKtXre9uu1>+D2leBW^* za=ni-`-}XKTcCi)^%)XYNeo)I$5o-wP`_mT%Sn!lsPCPt9g!Zgyyi~)2-+@0-T2m2 z_C3+}%p)h}gcs?jJMT`DLAFu4<3A% zo{<`NPC3%9NQy+eN4{6(%xV5EpE$Y9sJoz~)ZMQCO#gvS9V{!jmf0nH@w`7PWur!9 z{EDXe7oyPqp)3~9Qof(x>a$wi@+orU*}%Gg(REG~_PL!t(M>DPtEI3==(4cVOQ?wh zu-VRW{{9i`?OLbbP4zUplKQqMYRRfU5551fj9xWM6aKE|Iy8E!e>bJP`B8JWB<<1X z>;6&9ihikY8jdy7^GsQ#TcpV~1uE~r#sJ-{lO7nujA*3#k)ih;QzIVo5NH-g4klx2oy~7rfS34!{Aq%JfV^qhN{8(65FMBKjanvimN1@@1m}~t& zR^r~y&%AYg-R5aL?Owipz0?{xOIrA~8@sOfK-gBnV&{K2d7BaI`Yll~n>dm2-9Q(t zH)hGb$RueBkXTH_o8+9$jW1;Wqu^<2E{9MX$blV(sk8R3;N=* z-@2P2fn&|qmsKSS!{vv$Y~6dOM!oE_Rd_#ZcVpp{9K!wQO&wad z%^;~xA|4}hVqsf|Q)~z*aQ+(%*IC;^C|46_{Z~-=1ufCuch!M|KmWZGs8_E#=Nd-}-u0;1~7oSvJ-(W%vDYJxSFnpyQ{EcoWZ9zu&Kr zWQ;DSvz~6X89y2SRkKfXNKyJBpOX<;>gx1Q3_3YwPHwA3U$R1fru&KdgVLBP zO`zUJ1BMUNd-;vCNkT5E6W^mKemUzFcbsCV9B_9SAMk7x zg>ZU3JYw)aajX9X{m3t zM6|=y26mwfmo42$HS!pn$oKWI>le%GJr_t?Il>S3#Zi%HE(gTxCc?An`#Cv8;YuB5 z7TVI@C2LYKwum}vZ@GH~Akx=UzLTx=c9>-^yJZdC%Ey5R+JJUxbz`Z3>Xd0w#1j_1 zZ5pMs_naayT}V*$Qvud`wExLr60@aZX>1(KI&?Fz{8*ubXpo`;Gv^pCIm-QNGg09~ z9H-Q5$VuOBtG$g(P+4&CsR??2U`Z*bluzdWvf`8*Tw;Cl(VXiHJJOI$vzJHyj&CON zcPCIwyB!VP2~gPoZdJ*6U@v`>q`ZJat!Li2{>yz@^soo*=fip5O?RK4S2zyBa+g6@ zUKIWsY=JY++7wy*wa8df&ty)!y@HhPy7iJRxu*Y5sX{dSSHWsMpM|F{H+uc~CH*?@ z9-c(4Sc{D(a&T$5_<4<=`ae%kxmP=|_U5TCGU57q8PrbXaBRrfpru}Tgr-iTwyKYn zo?)2RWit0g^6$~H%%LygNS4#?cxtOVJJba0hm$`fn#0o>5u?Y)RAB!*H#WWd(;9kG@KTx zX*kKGeM6i5f<%3IgjofuK+?Pl`S26$$at{U55~jbc^-NzTH1vHIRedeX;(+aA0NrA z_ZBRO=EjI|pR8^V$jy}bD~CBg8rV&w`mK?Fif5BU;3^g*N-&l)W?Wu1Rv4rMf@|tr zYv5~+%q^|(&OEr@+1L}(R8}Vt5INo+w0H0QBN&h+VHDbh z89)x(S_#v7$cqcT5`uR_$}(jHh2S!4A-RpBANk60@#t!No;`$$6;NPooOPC&rqRzz zJAd`Ti43Bd;6n(N$s%TWPAt55`)_K$bUC13s!|x}BMYK0xf)+yo3UAi`Lf@u?9=5H zsqYfEvZ#m^Rv#fWGc~94MM}h&iyD>%UJJQ`BMp#;3ub^QHR91B$~mwd^ySY2AqwGj z3*3se@d{TmNf?}qZPHW2mbBkeaVLAi$C~G7kU`7|D6Q=t_T{?cYmeyO>F>>8MaBgp zQG)A%crTYep^3wC5s764BN`LCGHJ0~)MmPITyd>Q*oCl613g1I+>tT=dC2V#&}>@)LAd9DK!BWQjy)|+BR6u(+i{} zD~$NIPD9WP)ol!3{yza+OQNd-&UHRLt7o`1+m!v!VL-x1qXo{<8 ztUTzyL4SQU|C(u#!%sPYYWWoacI{A!G$PzYQ-8pbay#74zQ-in&S1@$=H z|H~m#EY|0qP$`gv4bWGB#UO$Paq^vlF2}HCw;~rGX?Jk^qlQ&j*~roc2gQa}?=i7@ z?1&Y-#Md{|at@YEL|{e@QM^B{?y?_S=m4nm2*GvTg>NBXoN#4eSrxRw}t4QQH}s0)^g*8%`ZBJanEQqBnjp5rRR$4ODGvO!>>h!yrXRk&HIdy zDF%-cw^JC8#rNxZ@w45d;^6AusS58o%ii7PYDO_M#>VPo1Iv+oOR>PFGD$?c$%x-snc$2MnC$*GRi>y@0-k*2537h%0yZMw=BfA->o zdoo-=dLn}DO#iZ8T8@2BeZI_ht{%@IU1_@T%QRAiYNOF@_V2LxMrapS03~dvH}X^Q zPkDGhL4EBg9GXtfyHPmF$4pgf%O@nt^CT}1e4?;vB^ zk^7=jgpm>OdPtM`r9JWG;o@U<%v<$wjDgSJ$FQExi{APF3S(-Nw^fDHTG66Eeh8%y zF54SbQ|O7cec|!Te)jy?f&JYZDuJ1AX+8Ey{9lu1{P_%%3Mw6q=g#Dw3NDB!Gz9;Z zz{aOr-!ASb4cq*me^bkB&dzyc_gLLgMiFvUFBPE*9J)@<#5J5FIA9^J~c;d8YtfOPt-a7?^{xjZ5elA+e z^a>~0vG6@EU|=WAF_0@^V_87A+!T2c9yhE05%0XjR zv_2%no{`_mM$I1 zj>O~eOxc<>=W~K+%(_OBQ9{p}`=%Td%t$jS(|e z2lnSw(!G)YNIg69@fXp1YRn~0%s(d{DeuC#`arH5RPuDa9Q7HSxkXk5L;iLtuOBLD zq+f!bkRB$r?kAVda4#u$M2GxiU#VUN&|k!f^PF5?XeHt!ObODG)EwUOa!Rq_;3V}} zce1^F5~%7Djb{FaM;jrbh$5rkCyI74NtvP_0zBcpGI2`IMDtJfF(Y|q6(aAR><-K^ zAOLccY=5za71jCxFN*BPha@`Mjc(I$a+lNhR7iXdaH+4xv<*N1G%`&z0 zDMqA^n&!Jja6lVMK59yjaj13=hZTnqb=#uy;cS(^TbyqCp+2@x5Kg@O)`aiw)*^Rt zrIx;F@rnENtsrSmO|J5O`Na5Q!m>db`z+2-_HPsA5F1nFq3qcAl|$LL^*HU_a@R5e zv9Qz#E!=ThJji+>3_v?80%7Aqd>kN921zkCk`ZNwBu>kr=B``r$Z99iOZ8L^+hi=V zth8;tB#W`A@*69CO%AA( z{ErrrLW&gV9m~+R=Na~cDIav>{DgRSa-uA{_C9%(rKG4c%)J|ARMcpqs#KqVrvsx8 zBYsx5G>1ci=am5Bn#jqImf559p5!0SADpclHBzVwDlXhcKb9>uuYVJFM!0u>S6{Y~ zkTGA}GW>!@WDx=nK%v6vf*Y2}Gz{mY#Xh8->21#qwHEiX1g!)ibSO!g)HioXU#qC9 z)i2V~rDGQ`Ds0c7Ww6NWjxTU@ir!RI!#@4qI47^bA009-6C=FYb9EK;kN!ei$@WIF zK|Gfc(ofP>GRtxNz=>8Q|7i%=qgz(Aga@npGse)`&2WO zYHgy>qY@m6LX68*j8BYPmIb@YT#hY4RSwb$szB7is39@s9kM=UE&oARzFA(Ip6Em) z%bbCt+!xDOb}gk+ToH0Oe1cuRDHSo|rL~AgQNtN`WO{)511uf0QT{a`s;RZ8lKm`4 zwgx;lb#3Lm6=JRg@>56av=Ujz@7ydzaE7NRU+A$1ayKDR(35}9aTzLe8e#a{t|pkV zk!s!}zgw5gBs+Vp@Qtc%IzoCYel@4C_k^E30h9C4;l}zFJ^>ROkp^9YD2&5FXJ+$S zE>boW83!9k;WR@{M|FylP#)B0w)cl3Ktc|GKk1Q5F0Gb&&5_g=Gu1UrC-jc~sM@nl zJf9KL7gp^Rmd}MAP9p^|miQHM5CJWjv`arR|4)^XIAiKwa7ZvZ8r)K>ICUr((^ZI+ z2PYO{gHUIMPlPhea3GBveix2fHTudiZOL>SF3yx{!}2cCwMX)Z20NUKw7eE;Fsi+g z(fagca;mi1Lh()6gM|@~-ur1=3n5cwEh1Cp`f3JAVkp0PjIubZRD-QeEuI<`c8<7- z(L?$lyWJvS?MpF@MisV1U!($1jWlg5r9w!pVriq%^ z4y6`5#e!CiMr&9qQ}DAP-_|wLkiGRLe~g7msFT4{h)MtD(tVT}$pgk;eNQU&tdH>g z^z`!h8R>ay*K*%i;K-Z^Fe#Wvy7@WBdzqnkf=Re^6+{z`ErJ~W-kei`Fvn4vz$gi$j+*y_y?UQ z6`iTY?;ilQY@Zg{YI23-TzMGozWmOujBMaq8nI+q&#r9K&C~jjk6d4_*Dm7FWKqrF zI}*Vy-#!wZTF4b8JnOo(R30=_9}D?~S}-r$D-fe2NozIaj8pW^?sn6xHl+$>W+8H zxH3IlAD(~)S=RMF_;Z}_Q)wnKd}f7yxXd_^csy-p_)MKdXd0~jIsXWbF5b*MjhAT% zBTN7IRIr(yA^Iim180oyA|buLk-V~0%m+4#A3tX|N{Q7JH^S<9u$#2yF*an7tFf`k zDtmwqRmn$8Yw#`+bXtywS&4$G#B#EmX+iVZ-txW)fpoFqkpZUNP79B<+M~A_(cbkN zhHT}ypb+nRDyTg%>nh zqekOKo*V6ZR2A-XGDc$SsnDuhHp@kgmCf$W4`qrV6m(gK!#-YkSWJa9!k;>-;nt}p zGDG-~KLeIlx*cevp?Fz3fS1}D8dhD3qKcbe93-wtmbjr?Dk}$LfHK+^AI(x?h8K4w zRwyGumwt=P%ar%c&768zdc;Z#2#l-ikEKQ@Zt+e$w1w`Wlb2gfz8jM#%5sGyqBvvx zoZ!{qNfLO&c`!taAWt6}w0_1(xXhhvuu7tsU7rq-j5=h1g$dM7}3`wo%R5_P-wO3Qn$;|>u|xL7LRvomx-n&Xvf0rQJSBc3`&82}PMq+~9AErD zuO+bhf7;oLsBJYqBn+YwTAXNx=3Ew##5aa}2ODXaT;-0{=m9_ae$LDm+zjHh2kTx3 zg$(wiYsnTjHQwC`zO)F69eJ7_Jf3bZ-t^rXouSVruscxf3?`CL^GiGV$ZZdnrU%55 zNbN_ByRv5#FC_}14AK_5#erw(md!qA(Cq10A4Q|ZR?@=%=JC-Y6X5es8GK0bYG204 z7;EY3?_y(6%bcr<5o&P?298Ajbtprh$e)28$_R#;R;UE=H7)KP$ab}HOin?dUA$SF zMdf^zuyacDX@1zeR2m+l!<_VZfmE=W3WY>xiLyU+tZTiTk*8DlVTM?Yk>>)UWStke zq=0^r8Ke(WBMorEyQ}-3>}6+F-43}?TFR*PD1A^A$=aVI@a7Ft7O{` zJ{4MZ6*sN@@o*5RHZkMcDllZBdBX$Eu&I?7zMT!cm%=Qrg~Rvx4r~1z(ClS?)ZW_OTQ`U&1Mfau@88<|YMf2kcpAKISXUD-dm&ps%}y`V2V+i? z7l}&y5za>XVEiK8Zd(BzzN%0bFfFbn|4lEKp`$%+D33XtMbIy7-^vF{676+Y#2G3_m$-WyOu7B#5vQDdB9 z4GquOwJ3ihu=|zj<==L0rK3gfNUb{xroFeBJXWw7QV&)L6}%Ku1Qr?x-4ptkwStNzLYMQCyJm}c#*r)q$d)SER_oe+GR@|M+Y<<)MJnsJ|8~Qu-REMG z+GE*ho-D8fSd<)YH7`f#=qMyk|1J9>uOs_@T-=e`%(91xut5Wrh-}a6$D|mE`pb%!s%?5jIY4`mz5DJG!fauyp-A&|1ZBVK%K+>4;j5L=lZX z1zMQ(pDF;r*rbLLlRyz)zx>}+fFe^bmHU5G063wfNVEk)%zmsh^l*b&*RvEa34u?* z^Hm5X1lca^YDD$mjUHe$^W6uU`@P9+i)YU+apv3ftQ*@m0WDd9UF`wSO+Fj<&hcL= z($3O713%fH_}^;gh&%LySLkBDM-@NKf95Z>bruh$JtF;;=G0nbbPesyYWZ&Rt8r=3 zGaylydF**42jYipwUTbA_UQFq6yX0gJ>a)rph(cWdyMypi$%>t7CP1Crajvvq^U{_ zk93~TQDroOVung*px6L!6S(F<*NeEtZV}`1X)8gc=8qq)kCt}4zwzNVz2xyol*)4F z?mp<)doQO|Xm+e{@hoU|kZc?_&3^ppH6v@d=+w%@^V$fE)z!WL(f$o%>OS|IwqiO< z4q&`1O>D06U3bq!73~vNr6O@}(~V*hV^|)GHk6aU!@c~vM-Fo`_pnmn_k7J2y{jw5 zWPy2kuiqqA#o}u7rA+{0UFMKK(nx%eyVu4)%Rr|7dc%5QdEdVN#Xr)1MiE+>Rq}=3 zJ{h$}U1edXJdh8;bYkfIx{Liy(`StVZatck!v-}IVFfLEMfYslvIyjNJT~X{xAe2h z@JR+BQdQhGg@cJA!O?X4*JOr&xc{=Fd(Wd>Ci_^)Hw`qUrk99}$$!A3_3u>5cA2Ou zi&!Jjlt{ELFpn{7R%?yssGDmHIgagBEKIxDuUI3V zfl)92ltszMvrUE0WQ4r;2tI|Ng73yN=i$e#kG@V`4Sw&3>`vp10&R({qcQoE7@EN5 z3BW(lcl+Dv*Px4=P2++;ywzI`p<<6O!KmQi=q_E=pY=^XMl%M?2B}k(wmfM{da}}A zKSxT%;J))adhEQ?---)AIKRUPFQ?B~^3{i0*-q+FMGd&Qxu|>FsYRJ~c#6cA2Pd*= zUm+`YQ!r!b^Ml47bHQXXQ3f=dGX@s;>FWv=xT%HycB1Y=zNdgS!>^F()IE1bUf3!@ znh@-D1fjFsyBZi#4w&K=Z$Z!tLB}^-p&wuDW1ZM_+jjOzWf6fDeqKbocGIxlX6?EcTQ82$3gCG%5!h7MI2mA* z)C$GUl|G=AT=K$YNCM^UInYMKY#ZZe3vAW2`9Bl>uRU@W&SH zB(u5$Rq{2J;MdC51S6!C7%6rf9CD4`Wa1=|jX#dzT(P5{|+F}i(azUPQ<;KQ$tVRgUx)%}|mC2Z^ zgZ*e%)8EZ+8Ew%dHZNDa^kM)Z=UZEt0nF_K2~nll0cel}hILWtYf zP_`ejn4^^D90{4DmQkA)lrmywv}Ps;UD1|uJfC05 z-?!<=3l%t{$j8-SljA4XD96IXn^CHWcKgWZ+QYi2XAU$X>0e&2hBD!y!Pd>~K1R(z z2bE52bM5&B<;s#h!3#<&JWw?Ow?Ilq1k|l`uQ#OQLuM^i&lTeV#V01zx0O*^sY>kU+yXCG=#NmG^1sEXejkG#od@LpG&v zS)*&$Ipm6>#R#k8_QaSR9dSA|SP1Sjk=Xb&S>D7njY4IJDdrFBR?Scu!VnSwqncx_ z3U*`M(6_&pNV?GM)HQa?I=Ir*sA zh4+Vu>KJZ_`8^#9bd`2wVE^=w8bRiqB3MV?{J&BoJOoR;IBj`9!lU-`DlQ#GK1Ig; z!UuhaHleF_6=DAFzl96LAOo^^f*NFnJxM4cPkvLa+Suo`ecTXr@K-)T$S~-l#;e$H z;Yfxe6=`;@Q$fdQ57mQO90&m#e3t4SlGr_s_WrC0Y`phoF(s;Z9Y6Z6<@7iFst4cm zhn?}xV6m-Hy#2jARS%oNU@UNpQ}aUgEl|7LTd29p@E`0Z2EZgl`}I z>O)sK0b2^9$2=SG^P#+naJ;QWf5~h0K3JqKQzgB@2BNf+eL=<@;Q(V6{#3Aq^T#9y zg^JnnehGcgH`(z-6+1n);oQ1~$e~9vb)fHmfcGROl41UNhn}3@15RY8MKoRUK>eDI zH2jRWz?uA`zJ0#;MOpxJumvy8zzx$`S% zpOM7B`ivM>_nYk~SCMo+xfAR|(>!ra7kq;ahDa=N)05?t7~PMhjqT;vo15486X_AzuZr@&^H74= zk5~$5c;JeJV3HO_iQ%P@qOh=3V2b|miH(F4z_2wO^JVi0#(&Dj|EZmxD)p=jGeO00 ztYcfK?xT{aC=Zg#qPOi%TwuVJu|kS0-I{XHsUl+a=|ekPL3M(5*q&+m+^uM4twK)tpMrR7pHHWzR?;#(?-U(Zm^n!d=@9lYofhSV(0Fv;}X2b#alUHi~i&r&hu@HfpWoc%qxlb0kGVew>9aC|*Drp(Qn*Wf` z>q~9Vqm;hO0Q6#P#A0R996^)qB$GR7v3S+O*)lNAkARzpB4{e~ok%;HseFUbspT(o zkCn5S23Mp7?a*FAA+h7}ex(=Bb7Q;o3Bk_Z2_VjZUTs0yZI%DaP&bdCk-OTL`@Oa@ z_G3l?Nax&b^HR=WsNHSliYQs$x9*2y8`h(c9E2^a>7JR`wvXSq$u6(LKHmtskM14a zUWH}$?eMxhqWbqte{%i}aUPkM7Ff*7%{7&ax!fZb+HE zi#buhcsz|U-)CUnmxq{on+8l5-~Gf+Nr=jFKLMnA4?6mCBv2?t zoA-%VrG+*X-+-=wx!2Rn1Lp=MB26) zQnI;^kn{o`B)frgsLUVKokLxOouBJ-^r-dJxR45&pNE^r!KY0bCAeq^;l&m9N+})kl^8ILWNcxbz7)N!pjdxYOEy0QkZ*RQOq~V*j|+F zH6HLd2?UdbEOlr4veZNL(yQtN0|=ADdOlZ6n1A1FK?KEFj}0)hW4N!loG7c zS~g&x4-S{c%et`xP(_gOho6zAp}W*@MidRaB1Cp+=<;}fBHyCcrK3Wk(Y1aaNqRt+ z2T)bgR*dpp!RO`bjjC@2#CrkQ9bYEf>{^N>)0wC4=WSl@*yhXd^}z?Vb7k~6^FPo- z?c}b^pqKReTeJI5f3(`9|F>3ayJkQCw^lpW9&ZF5;%IIQi|lXEpz7V;PsZSpC(bE~ zuPvp|>p9`J$ z7QiDxKE>=>vs^SwbIpTv=IL5A7<|V22m6zYow}+FJr-1lckhtv7CkSBnHpMfzdd5m z|Fnjk&~o0C7WwTt=JecwC}(Jtcr_%PdPQpeL^X2HwX0lMC1(&wQopWH3Ez^e|INzm zKFI26Sw+skG@+`y`YB#xWy8-={Asedc|(1r@2|7uRcP{)lcJX@U~B;R!|=~sE{1z#*8-_z>a zRQU=k=E3NbeXFo@jT5pRr_62bZlMO=-j-M?lAlv(vaU-~z^33mp8t5>p|YD9D{m;n z>LJn?1iKapdU8#3Dm=`f_P=H7B^E8YS(6faDk_e|E6&#-+Mtxt4~^bj*9Yj$j;)#WD{nDXh^#Q+Qu|JtqY35V|8^*| z$Sc<L^W^fg5-<`H-59ta=(<&vrrEFrq#4YG!mS`y+ejsGg)#sYdy(y|&wID0V7o z#C?&Jd(B(RFwhgOEo8P)vH%-Dq)GF3I2V1=(*D5Ys38vj+t7~=Z4qGc!$mD#Leu_B zleg2DBZ&gxM|_=Y`lY7w!^}LblL;qPFV&_F*UCok3PD5QSdmf@?vPqOf~S5=35;e4 zvgA?yC*VM^f>}`Jo%o~ZnzD$4=|>dW-|FtL*3x|BO@r7}OABR_*K{dsc`0ib+!Izq zRznLbLiJj@L962Rui#hqyA-l13Gr)@G#aL2y&SsGZMvSsY)Pv%9lGLBq~qLxo;EoP z*;ssqb&T~M(bCsZ0-qn|mOfa%UBNB@h{vMYqS-ohRlnwW{b1V_JZsVdv7X2}C6jEZ z(^zpJnj%lXxqc{^j?i=m&AL)0qynLzagG~E3jtSkYs96;Wtsta>(>`Pw?fCfv^2=V zvaIQ&t5tZEmCVjNnv>53Ton%gVInO4klp$FDN!PFlFCoC7>)upBoyOj=2#(735$(V zUayKbwY~oUT&cu{c!rsZC~?m}MI|KHEtiVnC*Y~NJ~ATg%$A*!6zhe$GV|7kA}%F~ z16RD?g(ezMvXm3uv9P_K?2W>1lqj$Qlv2TfDCTL4Gtl^2MXW?_wa%)Pyz6rWt{zsW zjpsVF)@?1=wYrXoT(1yH5=veVWw@8Lgw-0&El4r-NY3@pfhna>On$%4Gty4;) z%<9I9(j^R5WRR--SiYZNEXj0cN2sZee6Sp^_80;@>pNN-)$|na!2TNc9q{3y7jc6V zAW!-g^~<&~`j*VkG&cV4X_-!HEXOjs)q~J$7TFd>D6eH&rU=OSI{Q~HM6O#1)rsmA zhiIa|Lp7<`o^p_lUz8a{^s3z@CwM2Gtmys5##9hwULQQd|EjlSF~U-0=yi&1+uH|| zK_3+7uZFDQdAP=~633+Qb}PgV(-H7&2#%Wdd?+c0XV3jU*Dgn-JA%)IYWu4U6gzDq zQ-XsjM43c)u$+1Ew9PP0f;8UM16`c;w$@fsEy_j!$MPB^2%qKdr|F9WthW5^pY0ty z9qR$L3{^0;cTRFJM+}BYUfgo<1-gWXtsKG~nLppvXODn~oaj@CQO%C=Hp-k0)uNurBExVXwwrH&NL6u+z6+gL`I10Wlv2)NKwc& zl;2YJ?X+&XnhS7?a6!k-UX{MY6$__q-+z3Q=JjED_1B@<+vc=;4Qv-%%F7<PGR)r1&@b())Q&+i~>rY5suks8|#=4iFt;VWoGO;X`G=R`-x@6NRXHFdrVu`=Kdzr8wyLru?2-{H0`MOyPB6c9vySao|-ZiP4x;Hjw@E1=#a3$L+T3DDLdni)aBVrrWI0Q z;ums76)B+)_6K}&u^OcyM(J595Ryib>pje$4L~12jqyXVcoc7qYa|P9BB}J+cL>C4 znz;m_)C&-%Dk~^SIlh=$E>xFG@c$e!6gO??8W$8XC_zFD6h-k(#FkNX0dWhrNSb9| zhBZFWEe~@_!>EW?DP4T*y6^HM+50u}b8M+YkKw0ujZl((Xl`mQBDW>qNKqcN*lfY2 zCNGJzbQVaPOO!(7UKLv*pohs#|JUq04$=bJQ-z7_RbFc$ixFqxdhmdva4pg%%9^{a zKKHiq4J$WAZE{er?G&iH)R0fkbHm6&-`*rBk(8=NuZO7*S*lze1B#AXC{EvR#tcUZ zeQ-P=h|q*X(EC|9sVH0#?z~qIsVoxuSy{ho!+OAA{5k2*HHJy^BgH(#GQ{t# z7ok9VbOlLz6 zV@ZLJsQaifpqoazH@ZF|x0#bF8E>?1c2Jc>*byj{xJLU9`&|-en}YlYrJ(OQG7fdr z42_!-g*pT8@ZV4L$k}m62R}255A$&=FtC2Hz{5~Uo_9U`iU`JVU?&|WpFyD{!$gu_ zFPDRr-#>&&o(I7m1_$x5pZ$>Y;G1o*K^!x;Z4viX)1>MuO@R8Z%4%aLgLpPSTEy>; zH`uz1Qob%fRWCw1MbXM;H|}}cK&Z*OhHvToDPOaJP;iPyHHD=E6n=ADhp@*RXwg^k z4c}u?k6~emj+bt%Hc|?~g%mIXJj+nT0`k1)=lSn`;6Jk?*a0^9z3}@WtN#sM@ zQ|;LL<)obEH^F^dbxHCh?v;QeHH(KW2A{#xxbBUv$HhYTWePZM4sMsvgFi29Z7i?z z9+gc44EGVk2xA8lTvq6l{Rx5|ezYh;>HxaLsrWqDzA!O#bDd68{-Sj6a&cpGO+^O- zM;d-uPd@N@pERv!vzk`@T+SIKOM>K4rtGy2&!D_kPV-YpD`(cmmX0o6HUkaNn+8o4 z#KI=*spDDdNSK4sXQEogt(N*KxD8G-SeNvz%YRZZry~kU(Ph^w=VKPA#Tm`mEE=x9 z2{4RvEMF+KmtpIn8zAN(jIu!CfgvDu%=~8{vmaH&Vxd!+2#BCCC8omiPz?G=shyM#mn~HqwohhiU{%oOPliLDwm>%f;v!Pam1g2*xhOg^LvUlZ<(M*93o@t&={V zZO21&)pmspmxJKeIP~`!W|XQgg;-bcEGAJ8yMBke+*iec^v%%AecJJ4Y0%ha?k-O{ z1DskvfX`;;E!MQ!dPa{=+b)ENTgWE}SU!&xgy~YhRiPd|5DnO@mwd%(wQ8}~6$@iv z>^&w3Nb|h6Pu;Ueo?wY{R_pr|;|T=YoNe0`W;*^lv-y^`f(;Q8$(%U83=j@C&Kdvs zA_UCoHxTGHaTl&@(otY!m(B(!>bxeBnNSQGu|J7v>?pF=%^w{AyYUvU8a}Y6pYFXW zivx85K`$?;@4sD8@@1}y}7YUwq>jq(XoHpG9Fe{*d z8&$56hr<8aG^m?`bsdkq(|cbp)Rgq`H^7*2dK+{0E=kv`XuRi#c<1e28AU!>j;d*T z=nuNCyZ~31hoShYvO5i5hc8#w`OPfm1T5{klgjE|)kDVJd`I80mox2{LRv}(IE5#W zuq!;Ls?_)H_4;5&_KgKmQ|*kA1_S}XJ2oOLiR{pkg9bzQ=6ulilf?{$IF)nn-Er!k zlV@djnt|yAaX=l1P^@3`%;VZpPAO_TMK_?*pPgb_gs*%2OKzU&c=w~_FS>ix{B71p zzJR9IebZ8Bw(11PtU`KS;6@Ihi|?iNmo<&RB9FJ}XsXWT4(pWig9`UGd!YP-N_^Mz znx~w%2M*b^|MVW6Qq!`c=FQO3&#R7> { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ elements: [] }); + }); + }); + + describe('when there is data', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns service map elements', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + elements: [ + { + data: { + source: 'client', + target: 'opbeans-node', + id: 'client~opbeans-node', + sourceData: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: '>opbeans-java:3000', + id: 'opbeans-java~>opbeans-java:3000', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: '>postgresql', + id: 'opbeans-java~>postgresql', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: 'opbeans-node', + id: 'opbeans-java~opbeans-node', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + bidirectional: true, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>93.184.216.34:80', + id: 'opbeans-node~>93.184.216.34:80', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>postgresql', + id: 'opbeans-node~>postgresql', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>redis', + id: 'opbeans-node~>redis', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: 'opbeans-java', + id: 'opbeans-node~opbeans-java', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + isInverseEdge: true, + }, + }, + { + data: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + }, + { + data: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + }, + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, + }, + { + data: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + }, + { + data: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + { + data: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, + }, + ], + }); + }); + }); + }); +} From 43bfa4ab66aae5f9c2605b7b819b3fa6aefc5104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 30 Jun 2020 16:56:40 +0200 Subject: [PATCH 14/19] [ML] Modifies page title to Create job (#70191) Changes Create data frame analytics job to Create job. --- .../data_frame_analytics/pages/analytics_creation/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index ff718277a88a7..e821428890046 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -149,13 +149,13 @@ export const Page: FC = ({ jobId }) => { {jobId === undefined && ( )} {jobId !== undefined && ( )} From 04b8d108d5b6ed93527f0dfd04d634a0854fc321 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 30 Jun 2020 11:14:04 -0400 Subject: [PATCH 15/19] remove logs link and alerts count (#70282) --- .../view/details/host_details.tsx | 25 +------------------ .../pages/endpoint_hosts/view/hooks.ts | 16 ------------ .../pages/endpoint_hosts/view/index.test.tsx | 25 ------------------- 3 files changed, 1 insertion(+), 65 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 80c4e2f379c7c..66abf993770a7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -19,7 +19,7 @@ import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { HostMetadata } from '../../../../../../common/endpoint/types'; -import { useHostSelector, useHostLogsUrl, useAgentDetailsIngestUrl } from '../hooks'; +import { useHostSelector, useAgentDetailsIngestUrl } from '../hooks'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; @@ -51,7 +51,6 @@ const LinkToExternalApp = styled.div` const openReassignFlyoutSearch = '?openReassignFlyout=true'; export const HostDetails = memo(({ details }: { details: HostMetadata }) => { - const { url: logsUrl, appId: logsAppId, appPath: logsAppPath } = useHostLogsUrl(details.host.id); const agentId = details.elastic.agent.id; const { url: agentDetailsUrl, @@ -78,12 +77,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { }), description: , }, - { - title: i18n.translate('xpack.securitySolution.endpoint.host.details.alerts', { - defaultMessage: 'Alerts', - }), - description: '0', - }, ]; }, [details]); @@ -251,22 +244,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { listItems={detailsResultsLower} data-test-subj="hostDetailsLowerList" /> - - - - - - - - ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index c072c812edbb5..68198b691da40 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -21,22 +21,6 @@ export function useHostSelector(selector: (state: HostState) => TSele }); } -/** - * Returns an object that contains Kibana Logs app and URL information for a given host id - * @param hostId - */ -export const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); - return useMemo(() => { - const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; - return { - url: `${services.application.getUrlForApp('logs')}${appPath}`, - appId: 'logs', - appPath, - }; - }, [hostId, services.application]); -}; - /** * Returns an object that contains Ingest app and URL information */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 68943797ea07e..073e2a07457ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -278,7 +278,6 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); - coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); reactTestingLibrary.act(() => { history.push({ @@ -433,30 +432,6 @@ describe('when on the hosts page', () => { }); }); - it('should include the link to logs', async () => { - const renderResult = render(); - const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); - expect(linkToLogs).not.toBeNull(); - expect(linkToLogs.textContent).toEqual('Endpoint Logs'); - expect(linkToLogs.getAttribute('href')).toEqual( - "/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)" - ); - }); - - describe('when link to logs is clicked', () => { - beforeEach(async () => { - const renderResult = render(); - const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); - reactTestingLibrary.act(() => { - reactTestingLibrary.fireEvent.click(linkToLogs); - }); - }); - - it('should navigate to logs without full page refresh', () => { - expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); - }); - }); - describe('when showing host Policy Response panel', () => { let renderResult: ReturnType; beforeEach(async () => { From 2fe0051ec28fa565374584c4ecf0a4a4164674c1 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 30 Jun 2020 10:14:29 -0500 Subject: [PATCH 16/19] Index patterns - Server API (#69105) * index patterns on the server --- ...-data-public.indexpattern._constructor_.md | 4 +- ...plugin-plugins-data-public.indexpattern.md | 2 +- ...c.indexpatternattributes.fieldformatmap.md | 11 ++ ...lic.indexpatternattributes.intervalname.md | 11 ++ ...gins-data-public.indexpatternattributes.md | 3 + ...ic.indexpatternattributes.sourcefilters.md | 11 ++ ...r.indexpatternattributes.fieldformatmap.md | 11 ++ ...ver.indexpatternattributes.intervalname.md | 11 ++ ...gins-data-server.indexpatternattributes.md | 3 + ...er.indexpatternattributes.sourcefilters.md | 11 ++ ...plugin-plugins-data-server.plugin.start.md | 6 + ...s-data-server.pluginstart.indexpatterns.md | 11 ++ ...-plugin-plugins-data-server.pluginstart.md | 1 + ... => stubbed_saved_object_index_pattern.ts} | 8 +- .../data/common/index_patterns/index.ts | 1 + .../index_patterns/_fields_fetcher.ts | 3 +- .../ensure_default_index_pattern.ts | 10 +- .../index_patterns/index_patterns/index.ts | 1 - .../index_patterns/index_pattern.test.ts | 15 ++- .../index_patterns/index_pattern.ts | 102 ++++++++++------ .../index_patterns/index_patterns.test.ts | 39 +++--- .../index_patterns/index_patterns.ts | 73 +++++++----- .../data/common/index_patterns/lib/errors.ts | 2 +- .../data/common/index_patterns/types.ts | 52 ++++++++ .../data/common/index_patterns/utils.ts | 14 +-- .../data/public/index_patterns/index.ts | 9 +- .../index_patterns/index_patterns/index.ts | 1 + .../index_patterns_api_client.test.mock.ts | 0 .../index_patterns_api_client.test.ts | 0 .../index_patterns_api_client.ts | 17 +-- .../saved_objects_client_wrapper.ts | 67 +++++++++++ .../index_patterns/ui_settings_wrapper.ts | 45 +++++++ src/plugins/data/public/plugin.ts | 14 ++- src/plugins/data/public/public.api.md | 18 +-- .../data/server/index_patterns/index.ts | 2 +- .../index_patterns_api_client.ts | 29 +++++ .../index_patterns/index_patterns_service.ts | 45 ++++++- .../data/server/index_patterns/mocks.ts | 24 ++++ .../saved_objects_client_wrapper.ts | 53 +++++++++ .../index_patterns/ui_settings_wrapper.ts | 43 +++++++ src/plugins/data/server/mocks.ts | 2 + src/plugins/data/server/plugin.ts | 20 +++- src/plugins/data/server/server.api.md | 17 +++ test/plugin_functional/config.js | 1 + .../plugins/index_patterns/kibana.json | 9 ++ .../plugins/index_patterns/package.json | 17 +++ .../plugins/index_patterns/server/index.ts | 30 +++++ .../plugins/index_patterns/server/plugin.ts | 111 ++++++++++++++++++ .../plugins/index_patterns/tsconfig.json | 14 +++ .../test_suites/data_plugin/index.ts | 25 ++++ .../test_suites/data_plugin/index_patterns.ts | 64 ++++++++++ .../security/security_index_pattern_utils.ts | 7 +- .../use_create_analytics_form.ts | 3 +- 53 files changed, 945 insertions(+), 158 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md rename src/fixtures/{stubbed_saved_object_index_pattern.js => stubbed_saved_object_index_pattern.ts} (87%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts (100%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.test.ts (100%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.ts (87%) create mode 100644 src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts create mode 100644 src/plugins/data/public/index_patterns/ui_settings_wrapper.ts create mode 100644 src/plugins/data/server/index_patterns/index_patterns_api_client.ts create mode 100644 src/plugins/data/server/index_patterns/mocks.ts create mode 100644 src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts create mode 100644 src/plugins/data/server/index_patterns/ui_settings_wrapper.ts create mode 100644 test/plugin_functional/plugins/index_patterns/kibana.json create mode 100644 test/plugin_functional/plugins/index_patterns/package.json create mode 100644 test/plugin_functional/plugins/index_patterns/server/index.ts create mode 100644 test/plugin_functional/plugins/index_patterns/server/plugin.ts create mode 100644 test/plugin_functional/plugins/index_patterns/tsconfig.json create mode 100644 test/plugin_functional/test_suites/data_plugin/index.ts create mode 100644 test/plugin_functional/test_suites/data_plugin/index_patterns.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 6574e7ee37926..0268846772f2c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, }: IndexPatternDeps); +constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, } | IndexPatternDeps | | +| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index d39b384c538f1..bc999a3bb48e3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md new file mode 100644 index 0000000000000..9a454feab1e0e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) + +## IndexPatternAttributes.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md new file mode 100644 index 0000000000000..5902496fcd0e7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) + +## IndexPatternAttributes.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 39ae328c14501..eff2349f053ff 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -20,7 +20,10 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternattributes.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.indexpatternattributes.type.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md new file mode 100644 index 0000000000000..43966112b97c3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) + +## IndexPatternAttributes.sourceFilters property + +Signature: + +```typescript +sourceFilters?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md new file mode 100644 index 0000000000000..84cc8c705ff59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) + +## IndexPatternAttributes.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md new file mode 100644 index 0000000000000..77a0872546679 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) + +## IndexPatternAttributes.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 1fcc49796f59e..4a5b61f5c179b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -20,7 +20,10 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-server.indexpatternattributes.title.md) | string | | | [type](./kibana-plugin-plugins-data-server.indexpatternattributes.type.md) | string | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md new file mode 100644 index 0000000000000..10223a6353f10 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) + +## IndexPatternAttributes.sourceFilters property + +Signature: + +```typescript +sourceFilters?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 2c7a833ab641b..74bffc516725f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,6 +12,9 @@ start(core: CoreStart): { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }; ``` @@ -28,5 +31,8 @@ start(core: CoreStart): { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md new file mode 100644 index 0000000000000..02ed24e05bc10 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) > [indexPatterns](./kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md) + +## PluginStart.indexPatterns property + +Signature: + +```typescript +indexPatterns: IndexPatternsServiceStart; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md index 1377d82123d41..b878a179657ed 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md @@ -15,5 +15,6 @@ export interface DataPluginStart | Property | Type | Description | | --- | --- | --- | | [fieldFormats](./kibana-plugin-plugins-data-server.pluginstart.fieldformats.md) | FieldFormatsStart | | +| [indexPatterns](./kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md) | IndexPatternsServiceStart | | | [search](./kibana-plugin-plugins-data-server.pluginstart.search.md) | ISearchStart | | diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.ts similarity index 87% rename from src/fixtures/stubbed_saved_object_index_pattern.js rename to src/fixtures/stubbed_saved_object_index_pattern.ts index 8e0e230ef33dd..02e6cb85e341f 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.ts @@ -17,13 +17,13 @@ * under the License. */ +// @ts-expect-error import stubbedLogstashFields from './logstash_fields'; -import { SimpleSavedObject } from '../core/public'; const mockLogstashFields = stubbedLogstashFields(); -export function stubbedSavedObjectIndexPattern(id) { - return new SimpleSavedObject(undefined, { +export function stubbedSavedObjectIndexPattern(id: string | null = null) { + return { id, type: 'index-pattern', attributes: { @@ -32,5 +32,5 @@ export function stubbedSavedObjectIndexPattern(id) { fields: mockLogstashFields, }, version: 2, - }); + }; } diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index d26587efccc0f..51a642b775c29 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -19,3 +19,4 @@ export * from './fields'; export * from './types'; +export { IndexPatternsService } from './index_patterns'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts index 727c4d445688d..baeb1587d57a9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts @@ -17,7 +17,8 @@ * under the License. */ -import { GetFieldsOptions, IIndexPatternsApiClient, IndexPattern } from '.'; +import { IndexPattern } from '.'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; /** @internal */ export const createFieldsFetcher = ( diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index 2737627bf1977..26f1a185ada3a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -18,13 +18,13 @@ */ import { contains } from 'lodash'; -import { CoreStart } from 'kibana/public'; import { IndexPatternsContract } from './index_patterns'; +import { UiSettingsCommon } from '../types'; export type EnsureDefaultIndexPattern = () => Promise | undefined; export const createEnsureDefaultIndexPattern = ( - uiSettings: CoreStart['uiSettings'], + uiSettings: UiSettingsCommon, onRedirectNoIndexPattern: () => Promise | void ) => { /** @@ -33,12 +33,12 @@ export const createEnsureDefaultIndexPattern = ( */ return async function ensureDefaultIndexPattern(this: IndexPatternsContract) { const patterns = await this.getIds(); - let defaultId = uiSettings.get('defaultIndex'); + let defaultId = await uiSettings.get('defaultIndex'); let defined = !!defaultId; const exists = contains(patterns, defaultId); if (defined && !exists) { - uiSettings.remove('defaultIndex'); + await uiSettings.remove('defaultIndex'); defaultId = defined = false; } @@ -49,7 +49,7 @@ export const createEnsureDefaultIndexPattern = ( // If there is any index pattern created, set the first as default if (patterns.length >= 1) { defaultId = patterns[0]; - uiSettings.set('defaultIndex', defaultId); + await uiSettings.set('defaultIndex', defaultId); } else { return onRedirectNoIndexPattern(); } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts index 77527857ed0ca..31cd06b7dd0ea 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts @@ -17,7 +17,6 @@ * under the License. */ -export * from './index_patterns_api_client'; export * from './_pattern_cache'; export * from './flatten_hit'; export * from './format_hit'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index ba8e4f6fb3695..7fb1210fe1f32 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -66,7 +66,7 @@ const savedObjectsClient = { create: jest.fn(), get: jest.fn().mockImplementation(() => object), update: jest.fn().mockImplementation(async (type, id, body, { version }) => { - if (object._version !== version) { + if (object.version !== version) { throw new Object({ res: { status: 409, @@ -74,10 +74,10 @@ const savedObjectsClient = { }); } object.attributes.title = body.title; - object._version += 'a'; + object.version += 'a'; return { - id: object._id, - _version: object._version, + id: object.id, + version: object.version, }; }), }; @@ -109,6 +109,7 @@ function create(id: string, payload?: any): Promise { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); setDocsourcePayload(id, payload); @@ -382,8 +383,8 @@ describe('IndexPattern', () => { test('should handle version conflicts', async () => { setDocsourcePayload(null, { - _id: 'foo', - _version: 'foo', + id: 'foo', + version: 'foo', attributes: { title: 'something', }, @@ -397,6 +398,7 @@ describe('IndexPattern', () => { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); await pattern.init(); @@ -411,6 +413,7 @@ describe('IndexPattern', () => { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); await samePattern.init(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index e9ac5a09b9db3..bde550c660a32 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -19,25 +19,23 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SavedObjectsClientContract } from 'src/core/public'; -import { SavedObjectAttributes } from 'src/core/public'; +import { SavedObjectsClientCommon } from '../..'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; -import { - ES_FIELD_TYPES, - KBN_FIELD_TYPES, - IIndexPattern, - IFieldType, - UI_SETTINGS, -} from '../../../common'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { Field, IIndexPatternFieldList, getIndexPatternFieldListCreator } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { IIndexPatternsApiClient } from '.'; -import { OnNotification, OnError } from '../types'; +import { + OnNotification, + OnError, + UiSettingsCommon, + IIndexPatternsApiClient, + IndexPatternAttributes, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; @@ -45,16 +43,22 @@ import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -const type = 'index-pattern'; +const savedObjectType = 'index-pattern'; +interface IUiSettingsValues { + [key: string]: any; + shortDotsEnable: any; + metaFields: any; +} interface IndexPatternDeps { - getConfig: any; - savedObjectsClient: SavedObjectsClientContract; + getConfig: UiSettingsCommon['get']; + savedObjectsClient: SavedObjectsClientCommon; apiClient: IIndexPatternsApiClient; patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; + uiSettingsValues: IUiSettingsValues; } export class IndexPattern implements IIndexPattern { @@ -72,9 +76,9 @@ export class IndexPattern implements IIndexPattern { public metaFields: string[]; private version: string | undefined; - private savedObjectsClient: SavedObjectsClientContract; + private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; - private getConfig: any; + private getConfig: UiSettingsCommon['get']; private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private @@ -83,6 +87,7 @@ export class IndexPattern implements IIndexPattern { private onNotification: OnNotification; private onError: OnError; private apiClient: IIndexPatternsApiClient; + private uiSettingsValues: IUiSettingsValues; private mapping: MappingObject = expandShorthand({ title: ES_FIELD_TYPES.TEXT, @@ -116,6 +121,7 @@ export class IndexPattern implements IIndexPattern { fieldFormats, onNotification, onError, + uiSettingsValues, }: IndexPatternDeps ) { this.id = id; @@ -127,9 +133,10 @@ export class IndexPattern implements IIndexPattern { this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; + this.uiSettingsValues = uiSettingsValues; - this.shortDotsEnable = this.getConfig(UI_SETTINGS.SHORT_DOTS_ENABLE); - this.metaFields = this.getConfig(UI_SETTINGS.META_FIELDS); + this.shortDotsEnable = uiSettingsValues.shortDotsEnable; + this.metaFields = uiSettingsValues.metaFields; this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats, @@ -138,12 +145,8 @@ export class IndexPattern implements IIndexPattern { this.fields = this.createFieldList(this, [], this.shortDotsEnable); this.apiClient = apiClient; - this.fieldsFetcher = createFieldsFetcher( - this, - apiClient, - this.getConfig(UI_SETTINGS.META_FIELDS) - ); - this.flattenHit = flattenHitWrapper(this, this.getConfig(UI_SETTINGS.META_FIELDS)); + this.fieldsFetcher = createFieldsFetcher(this, apiClient, uiSettingsValues.metaFields); + this.flattenHit = flattenHitWrapper(this, uiSettingsValues.metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) @@ -160,7 +163,13 @@ export class IndexPattern implements IIndexPattern { private deserializeFieldFormatMap(mapping: any) { const FieldFormat = this.fieldFormats.getType(mapping.id); - return FieldFormat && new FieldFormat(mapping.params, this.getConfig); + return ( + FieldFormat && + new FieldFormat( + mapping.params, + (key: string) => this.uiSettingsValues[key]?.userValue || this.uiSettingsValues[key]?.value + ) + ); } private initFields(input?: any) { @@ -228,7 +237,7 @@ export class IndexPattern implements IIndexPattern { private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { if (!response.found) { - throw new SavedObjectNotFound(type, this.id, 'management/kibana/indexPatterns'); + throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); } _.forOwn(this.mapping, (fieldMapping: FieldMappingSpec, name: string | undefined) => { @@ -296,12 +305,22 @@ export class IndexPattern implements IIndexPattern { return this; // no id === no elasticsearch document } - const savedObject = await this.savedObjectsClient.get(type, this.id); + const savedObject = await this.savedObjectsClient.get( + savedObjectType, + this.id + ); const response = { - version: savedObject._version, - found: savedObject._version ? true : false, - ...(_.cloneDeep(savedObject.attributes) as SavedObjectAttributes), + version: savedObject.version, + found: savedObject.version ? true : false, + title: savedObject.attributes.title, + timeFieldName: savedObject.attributes.timeFieldName, + intervalName: savedObject.attributes.intervalName, + fields: savedObject.attributes.fields, + sourceFilters: savedObject.attributes.sourceFilters, + fieldFormatMap: savedObject.attributes.fieldFormatMap, + typeMeta: savedObject.attributes.typeMeta, + type: savedObject.attributes.type, }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); @@ -388,10 +407,10 @@ export class IndexPattern implements IIndexPattern { field.count = count; try { - const res = await this.savedObjectsClient.update(type, this.id, this.prepBody(), { + const res = await this.savedObjectsClient.update(savedObjectType, this.id, this.prepBody(), { version: this.version, }); - this.version = res._version; + this.version = res.version; } catch (e) { // no need for an error message here } @@ -462,13 +481,17 @@ export class IndexPattern implements IIndexPattern { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, + }, }); await duplicatePattern.destroy(); } const body = this.prepBody(); - const response = await this.savedObjectsClient.create(type, body, { id: this.id }); + const response = await this.savedObjectsClient.create(savedObjectType, body, { id: this.id }); this.id = response.id; return response.id; @@ -496,10 +519,10 @@ export class IndexPattern implements IIndexPattern { (key) => body[key] !== this.originalBody[key] ); return this.savedObjectsClient - .update(type, this.id, body, { version: this.version }) - .then((resp: any) => { + .update(savedObjectType, this.id, body, { version: this.version }) + .then((resp) => { this.id = resp.id; - this.version = resp._version; + this.version = resp.version; }) .catch((err) => { if ( @@ -514,7 +537,12 @@ export class IndexPattern implements IIndexPattern { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, + }, }); + return samePattern.init().then(() => { // What keys changed from now and what the server returned const updatedBody = samePattern.prepBody(); @@ -610,7 +638,7 @@ export class IndexPattern implements IIndexPattern { destroy() { if (this.id) { this.patternCache.clear(this.id); - return this.savedObjectsClient.delete(type, this.id); + return this.savedObjectsClient.delete(savedObjectType, this.id); } } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index b0ecfc89d376b..2eb9744fc16b3 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -19,12 +19,14 @@ // eslint-disable-next-line max-classes-per-file import { IndexPatternsService } from './index_patterns'; -import { SavedObjectsClientContract, SavedObjectsFindResponsePublic } from 'kibana/public'; -import { coreMock, httpServiceMock } from '../../../../../core/public/mocks'; import { fieldFormatsMock } from '../../field_formats/mocks'; +import { + UiSettingsCommon, + IIndexPatternsApiClient, + SavedObjectsClientCommon, + SavedObject, +} from '../types'; -const core = coreMock.createStart(); -const http = httpServiceMock.createStartContract(); const fieldFormats = fieldFormatsMock; jest.mock('./index_pattern', () => { @@ -39,33 +41,26 @@ jest.mock('./index_pattern', () => { }; }); -jest.mock('./index_patterns_api_client', () => { - class IndexPatternsApiClient { - getFieldsForWildcard = async () => ({}); - } - - return { - IndexPatternsApiClient, - }; -}); - describe('IndexPatterns', () => { let indexPatterns: IndexPatternsService; - let savedObjectsClient: SavedObjectsClientContract; + let savedObjectsClient: SavedObjectsClientCommon; beforeEach(() => { - savedObjectsClient = {} as SavedObjectsClientContract; + savedObjectsClient = {} as SavedObjectsClientCommon; savedObjectsClient.find = jest.fn( () => - Promise.resolve({ - savedObjects: [{ id: 'id', attributes: { title: 'title' } }], - }) as Promise> + Promise.resolve([{ id: 'id', attributes: { title: 'title' } }]) as Promise< + Array> + > ); indexPatterns = new IndexPatternsService({ - uiSettings: core.uiSettings, - savedObjectsClient, - http, + uiSettings: ({ + get: () => Promise.resolve(false), + getAll: () => {}, + } as any) as UiSettingsCommon, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientCommon, + apiClient: {} as IIndexPatternsApiClient, fieldFormats, onNotification: () => {}, onError: () => {}, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 5e51897d13372..ef03ca8fe2d14 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -17,25 +17,26 @@ * under the License. */ -import { - SavedObjectsClientContract, - SimpleSavedObject, - IUiSettingsClient, - HttpStart, - CoreStart, -} from 'src/core/public'; +import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; import { IndexPattern } from './index_pattern'; -import { IndexPatternsApiClient, GetFieldsOptions } from '.'; import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; import { getIndexPatternFieldListCreator, CreateIndexPatternFieldList, Field } from '../fields'; -import { IndexPatternSpec, FieldSpec } from '../types'; -import { OnNotification, OnError } from '../types'; +import { + OnNotification, + OnError, + UiSettingsCommon, + IIndexPatternsApiClient, + GetFieldsOptions, + FieldSpec, + IndexPatternSpec, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; +import { UI_SETTINGS, SavedObject } from '../../../common'; const indexPatternCache = createIndexPatternCache(); @@ -46,20 +47,20 @@ export interface IndexPatternSavedObjectAttrs { } interface IndexPatternsServiceDeps { - uiSettings: CoreStart['uiSettings']; - savedObjectsClient: SavedObjectsClientContract; - http: HttpStart; + uiSettings: UiSettingsCommon; + savedObjectsClient: SavedObjectsClientCommon; + apiClient: IIndexPatternsApiClient; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; - onRedirectNoIndexPattern: () => void; + onRedirectNoIndexPattern?: () => void; } export class IndexPatternsService { - private config: IUiSettingsClient; - private savedObjectsClient: SavedObjectsClientContract; - private savedObjectsCache?: Array> | null; - private apiClient: IndexPatternsApiClient; + private config: UiSettingsCommon; + private savedObjectsClient: SavedObjectsClientCommon; + private savedObjectsCache?: Array> | null; + private apiClient: IIndexPatternsApiClient; private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; @@ -74,13 +75,13 @@ export class IndexPatternsService { constructor({ uiSettings, savedObjectsClient, - http, + apiClient, fieldFormats, onNotification, onError, - onRedirectNoIndexPattern, + onRedirectNoIndexPattern = () => {}, }: IndexPatternsServiceDeps) { - this.apiClient = new IndexPatternsApiClient(http); + this.apiClient = apiClient; this.config = uiSettings; this.savedObjectsClient = savedObjectsClient; this.fieldFormats = fieldFormats; @@ -103,13 +104,11 @@ export class IndexPatternsService { } private async refreshSavedObjectsCache() { - this.savedObjectsCache = ( - await this.savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }) - ).savedObjects; + this.savedObjectsCache = await this.savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); } getIds = async (refresh: boolean = false) => { @@ -172,7 +171,7 @@ export class IndexPatternsService { }; getDefault = async () => { - const defaultIndexPatternId = this.config.get('defaultIndex'); + const defaultIndexPatternId = await this.config.get('defaultIndex'); if (defaultIndexPatternId) { return await this.get(defaultIndexPatternId); } @@ -191,7 +190,11 @@ export class IndexPatternsService { return indexPatternCache.set(id, indexPattern); }; - specToIndexPattern(spec: IndexPatternSpec) { + async specToIndexPattern(spec: IndexPatternSpec) { + const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const uiSettingsValues = await this.config.getAll(); + const indexPattern = new IndexPattern(spec.id, { getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, @@ -200,13 +203,18 @@ export class IndexPatternsService { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, }); indexPattern.initFromSpec(spec); return indexPattern; } - make = (id?: string): Promise => { + async make(id?: string): Promise { + const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const uiSettingsValues = await this.config.getAll(); + const indexPattern = new IndexPattern(id, { getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, @@ -215,10 +223,11 @@ export class IndexPatternsService { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, }); return indexPattern.init(); - }; + } } export type IndexPatternsContract = PublicMethodsOf; diff --git a/src/plugins/data/common/index_patterns/lib/errors.ts b/src/plugins/data/common/index_patterns/lib/errors.ts index 12efab7a2ca40..59019000f1924 100644 --- a/src/plugins/data/common/index_patterns/lib/errors.ts +++ b/src/plugins/data/common/index_patterns/lib/errors.ts @@ -19,7 +19,7 @@ /* eslint-disable */ -import { KbnError } from '../../../../kibana_utils/public'; +import { KbnError } from '../../../../kibana_utils/common/'; /** * Tried to call a method that relies on SearchSource having an indexPattern assigned diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 94121a274d686..4241df5718243 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -18,6 +18,8 @@ */ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; +// eslint-disable-next-line +import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES } from '..'; @@ -49,11 +51,61 @@ export interface IndexPatternAttributes { title: string; typeMeta: string; timeFieldName?: string; + intervalName?: string; + sourceFilters?: string; + fieldFormatMap?: string; } export type OnNotification = (toastInputFields: ToastInputFields) => void; export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; +export interface UiSettingsCommon { + get: (key: string) => Promise; + getAll: () => Promise>; + set: (key: string, value: any) => Promise; + remove: (key: string) => Promise; +} + +export interface SavedObjectsClientCommonFindArgs { + type: string | string[]; + fields?: string[]; + perPage?: number; + search?: string; + searchFields?: string[]; +} + +export interface SavedObjectsClientCommon { + find: (options: SavedObjectsClientCommonFindArgs) => Promise>>; + get: (type: string, id: string) => Promise>; + update: ( + type: string, + id: string, + attributes: Record, + options: Record + ) => Promise; + create: ( + type: string, + attributes: Record, + options: Record + ) => Promise; + delete: (type: string, id: string) => Promise<{}>; +} + +export interface GetFieldsOptions { + pattern?: string; + type?: string; + params?: any; + lookBack?: boolean; + metaFields?: string; +} + +export interface IIndexPatternsApiClient { + getFieldsForTimePattern: (options: GetFieldsOptions) => Promise; + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +} + +export type { SavedObject }; + export type AggregationRestrictions = Record< string, { diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index c3f9af62f8c0e..d9e1cfa0d952a 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -18,24 +18,24 @@ */ import { find } from 'lodash'; -import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; +import { SavedObjectsClientCommon, SavedObject } from '..'; /** * Returns an object matching a given title * - * @param client {SavedObjectsClientContract} + * @param client {SavedObjectsClientCommon} * @param title {string} - * @returns {Promise} + * @returns {Promise} */ export async function findByTitle( - client: SavedObjectsClientContract, + client: SavedObjectsClientCommon, title: string -): Promise | void> { +): Promise | void> { if (!title) { return Promise.resolve(); } - const { savedObjects } = await client.find({ + const savedObjects = await client.find({ type: 'index-pattern', perPage: 10, search: `"${title}"`, @@ -45,6 +45,6 @@ export async function findByTitle( return find( savedObjects, - (obj: SimpleSavedObject) => obj.get('title').toLowerCase() === title.toLowerCase() + (obj: SavedObject) => obj.attributes.title.toLowerCase() === title.toLowerCase() ); } diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 2c540527f468d..a6ee71c624f5a 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -34,4 +34,11 @@ export { IIndexPatternFieldList, } from '../../common/index_patterns'; -export { IndexPatternsService, IndexPatternsContract, IndexPattern } from './index_patterns'; +export { + IndexPatternsService, + IndexPatternsContract, + IndexPattern, + IndexPatternsApiClient, +} from './index_patterns'; +export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; +export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index_patterns/index.ts index 0db1c8c68b4ac..f63b48f877771 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index.ts @@ -19,3 +19,4 @@ export * from '../../../common/index_patterns/index_patterns'; export * from './redirect_no_index_pattern'; +export * from './index_patterns_api_client'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts similarity index 100% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts similarity index 100% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts similarity index 87% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index cd189ccf0135b..377a3f7f91a50 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -18,21 +18,12 @@ */ import { HttpSetup } from 'src/core/public'; -import { IndexPatternMissingIndices } from '../lib'; +import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../../common/index_patterns/types'; const API_BASE_URL: string = `/api/index_patterns/`; -export interface GetFieldsOptions { - pattern?: string; - type?: string; - params?: any; - lookBack?: boolean; - metaFields?: string; -} - -export type IIndexPatternsApiClient = PublicMethodsOf; - -export class IndexPatternsApiClient { +export class IndexPatternsApiClient implements IIndexPatternsApiClient { private http: HttpSetup; constructor(http: HttpSetup) { @@ -53,7 +44,7 @@ export class IndexPatternsApiClient { }); } - _getUrl(path: string[]) { + private _getUrl(path: string[]) { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } diff --git a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts new file mode 100644 index 0000000000000..8f1d02c5ffd54 --- /dev/null +++ b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; +import { SavedObjectsClient, SimpleSavedObject } from 'src/core/public'; +import { + SavedObjectsClientCommon, + SavedObjectsClientCommonFindArgs, + SavedObject, +} from '../../common/index_patterns'; + +type SOClient = Pick; + +const simpleSavedObjectToSavedObject = ( + simpleSavedObject: SimpleSavedObject +): SavedObject => ({ + version: simpleSavedObject._version, + ...omit(simpleSavedObject, '_version'), +}); + +export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommon { + private savedObjectClient: SOClient; + constructor(savedObjectClient: SOClient) { + this.savedObjectClient = savedObjectClient; + } + async find(options: SavedObjectsClientCommonFindArgs) { + const response = (await this.savedObjectClient.find(options)).savedObjects; + return response.map>(simpleSavedObjectToSavedObject); + } + + async get(type: string, id: string) { + const response = await this.savedObjectClient.get(type, id); + return simpleSavedObjectToSavedObject(response); + } + async update( + type: string, + id: string, + attributes: Record, + options: Record + ) { + const response = await this.savedObjectClient.update(type, id, attributes, options); + return simpleSavedObjectToSavedObject(response); + } + async create(type: string, attributes: Record, options: Record) { + const response = await this.savedObjectClient.create(type, attributes, options); + return simpleSavedObjectToSavedObject(response); + } + delete(type: string, id: string) { + return this.savedObjectClient.delete(type, id); + } +} diff --git a/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts b/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts new file mode 100644 index 0000000000000..17fc88ddd674d --- /dev/null +++ b/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IUiSettingsClient } from 'src/core/public'; +import { UiSettingsCommon } from '../../common/index_patterns'; + +export class UiSettingsPublicToCommon implements UiSettingsCommon { + private uiSettings: IUiSettingsClient; + constructor(uiSettings: IUiSettingsClient) { + this.uiSettings = uiSettings; + } + get(key: string) { + return Promise.resolve(this.uiSettings.get(key)); + } + + getAll() { + return Promise.resolve(this.uiSettings.getAll()); + } + + set(key: string, value: any) { + this.uiSettings.set(key, value); + return Promise.resolve(); + } + + remove(key: string) { + this.uiSettings.remove(key); + return Promise.resolve(); + } +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 51f96f10aa7c7..d5929cb9cd564 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -40,7 +40,12 @@ import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; -import { IndexPatternsService, onRedirectNoIndexPattern } from './index_patterns'; +import { + IndexPatternsService, + onRedirectNoIndexPattern, + IndexPatternsApiClient, + UiSettingsPublicToCommon, +} from './index_patterns'; import { setFieldFormats, setHttp, @@ -76,6 +81,7 @@ import { ACTION_VALUE_CLICK, ValueClickActionContext, } from './actions/value_click_action'; +import { SavedObjectsClientPublicToCommon } from './index_patterns'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -164,9 +170,9 @@ export class DataPublicPlugin implements Plugin { notifications.toasts.add(toastInputFields); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0bb3fc3a3bf16..f2c7a907cda1d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -66,8 +66,6 @@ import { GetSourceParams } from 'elasticsearch'; import { GetTemplateParams } from 'elasticsearch'; import { History } from 'history'; import { Href } from 'history'; -import { HttpSetup } from 'src/core/public'; -import { HttpStart } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { IndexDocumentParams } from 'elasticsearch'; import { IndicesAnalyzeParams } from 'elasticsearch'; @@ -148,15 +146,15 @@ import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject as SavedObject_2 } from 'src/core/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObject } from 'src/core/server'; +import { SavedObject as SavedObject_3 } from 'src/core/public'; +import { SavedObjectsClientContract as SavedObjectsClientContract_3 } from 'src/core/public'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/public'; -import { SimpleSavedObject } from 'src/core/public'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; import { SnapshotDeleteParams } from 'elasticsearch'; @@ -300,7 +298,7 @@ export const connectToQueryState: ({ timefilter: { timefil // Warning: (ae-missing-release-tag) "createSavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract) => SavedQueryService; +export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract_3) => SavedQueryService; // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -982,7 +980,7 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, }: IndexPatternDeps); + constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); // (undocumented) [key: string]: any; // (undocumented) @@ -1097,9 +1095,15 @@ export type IndexPatternAggRestrictions = Record { +export interface IndexPatternsServiceStart { + indexPatternsServiceFactory: ( + kibanaRequest: KibanaRequest + ) => Promise; +} + +export interface IndexPatternsServiceStartDeps { + fieldFormats: FieldFormatsStart; + logger: Logger; +} + +export class IndexPatternsService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); @@ -30,5 +46,28 @@ export class IndexPatternsService implements Plugin { registerRoutes(core.http); } - public start() {} + public start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps) { + const { uiSettings, savedObjects } = core; + + return { + indexPatternsServiceFactory: async (kibanaRequest: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(kibanaRequest); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return new IndexPatternsCommonService({ + uiSettings: new UiSettingsServerToCommon(uiSettingsClient), + savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), + apiClient: new IndexPatternsApiServer(), + fieldFormats: formats, + onError: (error) => { + logger.error(error); + }, + onNotification: ({ title, text }) => { + logger.warn(`${title} : ${text}`); + }, + }); + }, + }; + } } diff --git a/src/plugins/data/server/index_patterns/mocks.ts b/src/plugins/data/server/index_patterns/mocks.ts new file mode 100644 index 0000000000000..8f95afe3b3c9d --- /dev/null +++ b/src/plugins/data/server/index_patterns/mocks.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function createIndexPatternsStartMock() { + return { + indexPatternsServiceFactory: jest.fn(), + }; +} diff --git a/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts new file mode 100644 index 0000000000000..c82695b7cb2ba --- /dev/null +++ b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; +import { + SavedObjectsClientCommon, + SavedObjectsClientCommonFindArgs, +} from '../../common/index_patterns'; + +export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { + private savedObjectClient: SavedObjectsClientContract; + constructor(savedObjectClient: SavedObjectsClientContract) { + this.savedObjectClient = savedObjectClient; + } + async find(options: SavedObjectsClientCommonFindArgs) { + const result = await this.savedObjectClient.find(options); + return result.saved_objects; + } + + async get(type: string, id: string) { + return await this.savedObjectClient.get(type, id); + } + async update( + type: string, + id: string, + attributes: Record, + options: Record + ) { + return (await this.savedObjectClient.update(type, id, attributes, options)) as SavedObject; + } + async create(type: string, attributes: Record, options: Record) { + return await this.savedObjectClient.create(type, attributes, options); + } + delete(type: string, id: string) { + return this.savedObjectClient.delete(type, id); + } +} diff --git a/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts b/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts new file mode 100644 index 0000000000000..34cdfdff0b80f --- /dev/null +++ b/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IUiSettingsClient } from 'src/core/server'; +import { UiSettingsCommon } from '../../common/index_patterns'; + +export class UiSettingsServerToCommon implements UiSettingsCommon { + private uiSettings: IUiSettingsClient; + constructor(uiSettings: IUiSettingsClient) { + this.uiSettings = uiSettings; + } + get(key: string) { + return this.uiSettings.get(key); + } + + getAll() { + return this.uiSettings.getAll(); + } + + set(key: string, value: any) { + return this.uiSettings.set(key, value); + } + + remove(key: string) { + return this.uiSettings.remove(key); + } +} diff --git a/src/plugins/data/server/mocks.ts b/src/plugins/data/server/mocks.ts index e2f2298234054..785e4a1ec41ab 100644 --- a/src/plugins/data/server/mocks.ts +++ b/src/plugins/data/server/mocks.ts @@ -19,6 +19,7 @@ import { createSearchSetupMock, createSearchStartMock } from './search/mocks'; import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks'; +import { createIndexPatternsStartMock } from './index_patterns/mocks'; function createSetupContract() { return { @@ -31,6 +32,7 @@ function createStartContract() { return { search: createSearchStartMock(), fieldFormats: createFieldFormatsStartMock(), + indexPatterns: createIndexPatternsStartMock(), }; } diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 0edce458f1c6b..bcf1f4f8ab60b 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -17,9 +17,15 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; import { ConfigSchema } from '../config'; -import { IndexPatternsService } from './index_patterns'; +import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns'; import { ISearchSetup, ISearchStart } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; @@ -38,6 +44,7 @@ export interface DataPluginSetup { export interface DataPluginStart { search: ISearchStart; fieldFormats: FieldFormatsStart; + indexPatterns: IndexPatternsServiceStart; } export interface DataPluginSetupDependencies { @@ -52,12 +59,14 @@ export class DataServerPlugin implements Plugin) { this.searchService = new SearchService(initializerContext); this.scriptsService = new ScriptsService(); this.kqlTelemetryService = new KqlTelemetryService(initializerContext); this.autocompleteService = new AutocompleteService(initializerContext); + this.logger = initializerContext.logger.get('data'); } public setup( @@ -79,9 +88,14 @@ export class DataServerPlugin implements Plugin { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }; // (undocumented) stop(): void; @@ -681,6 +694,10 @@ export interface PluginStart { // // (undocumented) fieldFormats: FieldFormatsStart; + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + indexPatterns: IndexPatternsServiceStart; // (undocumented) search: ISearchStart; } diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 078eb9ee88a8e..f51fb5e1bade4 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -38,6 +38,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/management'), require.resolve('./test_suites/doc_views'), require.resolve('./test_suites/application_links'), + require.resolve('./test_suites/data_plugin'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/index_patterns/kibana.json b/test/plugin_functional/plugins/index_patterns/kibana.json new file mode 100644 index 0000000000000..e098950dc9677 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "index_patterns_test_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["index_patterns_test_plugin"], + "server": true, + "ui": false, + "requiredPlugins": ["data"] +} diff --git a/test/plugin_functional/plugins/index_patterns/package.json b/test/plugin_functional/plugins/index_patterns/package.json new file mode 100644 index 0000000000000..eaba6ca624bd8 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/package.json @@ -0,0 +1,17 @@ +{ + "name": "index_patterns_test_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/index_patterns_test_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/index_patterns/server/index.ts b/test/plugin_functional/plugins/index_patterns/server/index.ts new file mode 100644 index 0000000000000..0c99dd30c9cb6 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/server/index.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/server'; +import { + IndexPatternsTestPlugin, + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart +> = () => new IndexPatternsTestPlugin(); diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts new file mode 100644 index 0000000000000..ffc70136ccffa --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Plugin } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; + +export interface IndexPatternsTestStartDeps { + data: DataPluginStart; +} + +export class IndexPatternsTestPlugin + implements + Plugin< + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart, + {}, + IndexPatternsTestStartDeps + > { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { path: '/api/index-patterns-plugin/get-all', validate: false }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ids = await service.getIds(); + return res.ok({ body: ids }); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/get/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const id = (req.params as Record).id; + const [, { data }] = await core.getStartServices(); + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + return res.ok({ body: ip.toSpec() }); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/update/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const id = (req.params as Record).id; + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + await ip.save(); + return res.ok(); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/delete/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const id = (req.params as Record).id; + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + await ip.destroy(); + return res.ok(); + } + ); + } + + public start() {} + public stop() {} +} + +export type IndexPatternsTestPluginSetup = ReturnType; +export type IndexPatternsTestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json new file mode 100644 index 0000000000000..6f0c32ad30601 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "server/**/*.ts", + "server/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts new file mode 100644 index 0000000000000..1c3f118135ffa --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-expect-error +export default function ({ loadTestFile }) { + describe('data plugin', () => { + loadTestFile(require.resolve('./index_patterns')); + }); +} diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts new file mode 100644 index 0000000000000..481e9d76e3acc --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'settings']); + + describe('index patterns', function () { + let indexPatternId = ''; + before(async () => { + await esArchiver.loadIfNeeded( + '../functional/fixtures/es_archiver/getting_started/shakespeare' + ); + await PageObjects.common.navigateToApp('settings'); + await PageObjects.settings.createIndexPattern('shakespeare', ''); + }); + + it('can get all ids', async () => { + const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) + .body; + indexPatternId = body[0]; + expect(body.length > 0).to.equal(true); + }); + + it('can get index pattern by id', async () => { + const body = await ( + await supertest.get(`/api/index-patterns-plugin/get/${indexPatternId}`).expect(200) + ).body; + expect(body.fields.length > 0).to.equal(true); + }); + + it('can update index pattern', async () => { + const body = await ( + await supertest.get(`/api/index-patterns-plugin/update/${indexPatternId}`).expect(200) + ).body; + expect(body).to.eql({}); + }); + + it('can delete index pattern', async () => { + await supertest.get(`/api/index-patterns-plugin/delete/${indexPatternId}`).expect(200); + }); + }); +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts index 141b9133505b7..6ba27322757bf 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts @@ -6,9 +6,6 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import minimatch from 'minimatch'; -import { SimpleSavedObject } from 'src/core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IndexPatternSavedObjectAttrs } from 'src/plugins/data/common/index_patterns/index_patterns/index_patterns'; import { getIndexPatternService, getUiSettings } from '../../../../kibana_services'; export type IndexPatternMeta = { @@ -29,13 +26,13 @@ export async function getSecurityIndexPatterns(): Promise { const indexPatternCache = await getIndexPatternService().getCache(); return indexPatternCache! - .filter((savedObject: SimpleSavedObject) => { + .filter((savedObject) => { return (securityIndexPatternTitles as string[]).some((indexPatternTitle) => { // glob matching index pattern title return minimatch(indexPatternTitle, savedObject?.attributes?.title); }); }) - .map((savedObject: SimpleSavedObject) => { + .map((savedObject) => { return { id: savedObject.id, title: savedObject.attributes.title, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 2de9a1dcadd4b..f95d2f572a406 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -8,7 +8,6 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { SimpleSavedObject } from 'kibana/public'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; @@ -235,7 +234,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { // Set the index pattern titles which the user can choose as the source. const indexPatternsMap: SourceIndexMap = {}; const savedObjects = (await mlContext.indexPatterns.getCache()) || []; - savedObjects.forEach((obj: SimpleSavedObject>) => { + savedObjects.forEach((obj) => { const title = obj?.attributes?.title; if (title !== undefined) { const id = obj?.id || ''; From 7c9db862abc1632bc758c4994e4a2133407d8133 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 30 Jun 2020 12:07:06 -0400 Subject: [PATCH 17/19] [Ingest Manager] Do not index every saved object field (#70162) --- .../server/saved_objects/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 470b808420136..1d412937e244f 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -65,9 +65,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { config_revision: { type: 'integer' }, config_newest_revision: { type: 'integer' }, default_api_key_id: { type: 'keyword' }, - default_api_key: { type: 'keyword' }, + default_api_key: { type: 'binary', index: false }, updated_at: { type: 'date' }, - current_error_events: { type: 'text' }, + current_error_events: { type: 'text', index: false }, packages: { type: 'keyword' }, }, }, @@ -83,7 +83,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { agent_id: { type: 'keyword' }, type: { type: 'keyword' }, - data: { type: 'binary' }, + data: { type: 'binary', index: false }, sent_at: { type: 'date' }, created_at: { type: 'date' }, }, @@ -130,7 +130,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, - monitoring_enabled: { type: 'keyword' }, + monitoring_enabled: { type: 'keyword', index: false }, }, }, migrations: { @@ -148,7 +148,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { name: { type: 'keyword' }, type: { type: 'keyword' }, - api_key: { type: 'binary' }, + api_key: { type: 'binary', index: false }, api_key_id: { type: 'keyword' }, config_id: { type: 'keyword' }, created_at: { type: 'date' }, @@ -171,9 +171,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { type: { type: 'keyword' }, is_default: { type: 'boolean' }, hosts: { type: 'keyword' }, - ca_sha256: { type: 'keyword' }, - fleet_enroll_username: { type: 'binary' }, - fleet_enroll_password: { type: 'binary' }, + ca_sha256: { type: 'keyword', index: false }, + fleet_enroll_username: { type: 'binary', index: false }, + fleet_enroll_password: { type: 'binary', index: false }, config: { type: 'flattened' }, }, }, @@ -202,6 +202,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { output_id: { type: 'keyword' }, inputs: { type: 'nested', + enabled: false, properties: { type: { type: 'keyword' }, enabled: { type: 'boolean' }, From 2118439d8705a4329b2a525de654ee659a152c21 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 30 Jun 2020 13:53:53 -0400 Subject: [PATCH 18/19] [Ingest Manager] Make setupIngestManager wait if setup is in progress (#70008) * Make setupIngestManager wait if another setup is in progress --- .../ingest_manager/server/services/setup.ts | 137 ++++++++++-------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 7a81a1db84b60..9cf1e5b368385 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -30,70 +30,93 @@ import { appContextService } from './app_context'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; +// the promise which tracks the setup +let setupIngestStatus: Promise | undefined; +// default resolve & reject to guard against "undefined is not a function" errors +let onSetupResolve = () => {}; +let onSetupReject = (error: Error) => {}; + export async function setupIngestManager( soClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser ) { - const [installedPackages, defaultOutput, config] = await Promise.all([ - // packages installed by default - ensureInstalledDefaultPackages(soClient, callCluster), - outputService.ensureDefaultOutput(soClient), - agentConfigService.ensureDefaultAgentConfig(soClient), - ensureDefaultIndices(callCluster), - settingsService.getSettings(soClient).catch((e: any) => { - if (e.isBoom && e.output.statusCode === 404) { - const http = appContextService.getHttpSetup(); - const serverInfo = http.getServerInfo(); - const basePath = http.basePath; - - const cloud = appContextService.getCloud(); - const cloudId = cloud?.isCloudEnabled && cloud.cloudId; - const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; - const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; - const defaultUrl = url.format({ - protocol: serverInfo.protocol, - hostname: serverInfo.host, - port: serverInfo.port, - pathname: basePath.serverBasePath, - }); - - return settingsService.saveSettings(soClient, { - agent_auto_upgrade: true, - package_auto_upgrade: true, - kibana_url: cloudUrl || flagsUrl || defaultUrl, - }); - } - - return Promise.reject(e); - }), - ]); - - // ensure default packages are added to the default conifg - const configWithDatasource = await agentConfigService.get(soClient, config.id, true); - if (!configWithDatasource) { - throw new Error('Config not found'); - } - if ( - configWithDatasource.datasources.length && - typeof configWithDatasource.datasources[0] === 'string' - ) { - throw new Error('Config not found'); + // installation in progress + if (setupIngestStatus) { + await setupIngestStatus; + } else { + // create the initial promise + setupIngestStatus = new Promise((res, rej) => { + onSetupResolve = res; + onSetupReject = rej; + }); } - for (const installedPackage of installedPackages) { - const packageShouldBeInstalled = DEFAULT_AGENT_CONFIGS_PACKAGES.some( - (packageName) => installedPackage.name === packageName - ); - if (!packageShouldBeInstalled) { - continue; + try { + const [installedPackages, defaultOutput, config] = await Promise.all([ + // packages installed by default + ensureInstalledDefaultPackages(soClient, callCluster), + outputService.ensureDefaultOutput(soClient), + agentConfigService.ensureDefaultAgentConfig(soClient), + ensureDefaultIndices(callCluster), + settingsService.getSettings(soClient).catch((e: any) => { + if (e.isBoom && e.output.statusCode === 404) { + const http = appContextService.getHttpSetup(); + const serverInfo = http.getServerInfo(); + const basePath = http.basePath; + + const cloud = appContextService.getCloud(); + const cloudId = cloud?.isCloudEnabled && cloud.cloudId; + const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; + const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; + const defaultUrl = url.format({ + protocol: serverInfo.protocol, + hostname: serverInfo.host, + port: serverInfo.port, + pathname: basePath.serverBasePath, + }); + + return settingsService.saveSettings(soClient, { + agent_auto_upgrade: true, + package_auto_upgrade: true, + kibana_url: cloudUrl || flagsUrl || defaultUrl, + }); + } + + return Promise.reject(e); + }), + ]); + + // ensure default packages are added to the default conifg + const configWithDatasource = await agentConfigService.get(soClient, config.id, true); + if (!configWithDatasource) { + throw new Error('Config not found'); } + if ( + configWithDatasource.datasources.length && + typeof configWithDatasource.datasources[0] === 'string' + ) { + throw new Error('Config not found'); + } + for (const installedPackage of installedPackages) { + const packageShouldBeInstalled = DEFAULT_AGENT_CONFIGS_PACKAGES.some( + (packageName) => installedPackage.name === packageName + ); + if (!packageShouldBeInstalled) { + continue; + } - const isInstalled = configWithDatasource.datasources.some((d: Datasource | string) => { - return typeof d !== 'string' && d.package?.name === installedPackage.name; - }); - - if (!isInstalled) { - await addPackageToConfig(soClient, installedPackage, configWithDatasource, defaultOutput); + const isInstalled = configWithDatasource.datasources.some((d: Datasource | string) => { + return typeof d !== 'string' && d.package?.name === installedPackage.name; + }); + if (!isInstalled) { + await addPackageToConfig(soClient, installedPackage, configWithDatasource, defaultOutput); + } } + + // if everything works, resolve/succeed + onSetupResolve(); + } catch (error) { + // if anything errors, reject/fail + onSetupReject(error); } } @@ -135,7 +158,7 @@ export async function setupFleet( }, }); - await outputService.invalidateCache(); + outputService.invalidateCache(); // save fleet admin user const defaultOutputId = await outputService.getDefaultOutputId(soClient); From bb7bc782b2ffa8848fcd7ac84f7c0210078ae490 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Tue, 30 Jun 2020 13:56:35 -0400 Subject: [PATCH 19/19] [Logs and Metrics UI] Initial setup for registering observability overview data fetchers (#69999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switches mount callbacks to only use start deps Fixes #58014 * Sets up skeleton logs data fetchers for overview * Fixes type hacks for logs fetcher * Prevent kibana from crashing on initial load * Fixes types and linting errors * Fixes some linting import/export issues Co-authored-by: Alejandro Fernández Gómez --- src/plugins/usage_collection/public/index.ts | 2 +- .../services/rest/observability_dashboard.ts | 7 +- .../get_transaction_coordinates.ts | 2 +- x-pack/plugins/infra/kibana.json | 4 +- .../infra/public/apps/common_providers.tsx | 4 +- x-pack/plugins/infra/public/apps/logs_app.tsx | 6 +- .../plugins/infra/public/apps/metrics_app.tsx | 6 +- x-pack/plugins/infra/public/index.ts | 17 ++-- x-pack/plugins/infra/public/plugin.ts | 63 ++++++------- x-pack/plugins/infra/public/types.ts | 30 +++++- .../public/utils/logs_overview_fetchers.ts | 93 +++++++++++++++++++ .../observability/public/data_handler.ts | 22 +---- x-pack/plugins/observability/public/index.ts | 8 +- x-pack/plugins/observability/public/plugin.ts | 4 +- .../index.ts} | 22 +++++ .../observability/public/typings/index.ts | 1 + 16 files changed, 201 insertions(+), 90 deletions(-) create mode 100644 x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts rename x-pack/plugins/observability/public/typings/{fetch_data_response/index.d.ts => fetch_overview_data/index.ts} (68%) diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index 712e6a76152a2..c6c6ba64e6630 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -21,7 +21,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export { UsageCollectionSetup } from './plugin'; +export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new UsageCollectionPlugin(initializerContext); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 2221904932b63..4614e06cbd45d 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -6,9 +6,10 @@ import { i18n } from '@kbn/i18n'; import { sum } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FetchDataParams } from '../../../../observability/public/data_handler'; -import { ApmFetchDataResponse } from '../../../../observability/public/typings/fetch_data_response'; +import { + ApmFetchDataResponse, + FetchDataParams, +} from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; import { Theme } from '../../utils/get_theme'; diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts index 78ed11d839ad2..e78a3c1cec24a 100644 --- a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { rangeFilter } from '../../../common/utils/range_filter'; -import { Coordinates } from '../../../../observability/public/typings/fetch_data_response'; +import { Coordinates } from '../../../../observability/public'; import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 4e23f1985d450..e5ce1b1cd96f8 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -13,9 +13,7 @@ "alerts", "triggers_actions_ui" ], - "optionalPlugins": [ - "ml" - ], + "optionalPlugins": ["ml", "observability"], "server": true, "ui": true, "configPath": ["xpack", "infra"] diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index facb0f1539a10..9e4917856d8b2 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -12,7 +12,7 @@ import { KibanaContextProvider, } from '../../../../../src/plugins/kibana_react/public'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; -import { ClientPluginDeps } from '../types'; +import { InfraClientStartDeps } from '../types'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; import { ApolloClientContext } from '../utils/apollo_context'; import { EuiThemeProvider } from '../../../observability/public'; @@ -37,7 +37,7 @@ export const CommonInfraProviders: React.FC<{ export const CoreProviders: React.FC<{ core: CoreStart; - plugins: ClientPluginDeps; + plugins: InfraClientStartDeps; }> = ({ children, core, plugins }) => { return ( diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index e0251522bb24c..528d90b2a3a23 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -15,14 +15,14 @@ import '../index.scss'; import { NotFoundPage } from '../pages/404'; import { LinkToLogsPage } from '../pages/link_to/link_to_logs'; import { LogsPage } from '../pages/logs'; -import { ClientPluginDeps } from '../types'; +import { InfraClientStartDeps } from '../types'; import { createApolloClient } from '../utils/apollo_client'; import { CommonInfraProviders, CoreProviders } from './common_providers'; import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, - plugins: ClientPluginDeps, + plugins: InfraClientStartDeps, { element, history }: AppMountParameters ) => { const apolloClient = createApolloClient(core.http.fetch); @@ -43,7 +43,7 @@ const LogsApp: React.FC<{ apolloClient: ApolloClient<{}>; core: CoreStart; history: History; - plugins: ClientPluginDeps; + plugins: InfraClientStartDeps; }> = ({ apolloClient, core, history, plugins }) => { const uiCapabilities = core.application.capabilities; diff --git a/x-pack/plugins/infra/public/apps/metrics_app.tsx b/x-pack/plugins/infra/public/apps/metrics_app.tsx index 8713abe0510a6..3069490466938 100644 --- a/x-pack/plugins/infra/public/apps/metrics_app.tsx +++ b/x-pack/plugins/infra/public/apps/metrics_app.tsx @@ -16,7 +16,7 @@ import { NotFoundPage } from '../pages/404'; import { LinkToMetricsPage } from '../pages/link_to/link_to_metrics'; import { InfrastructurePage } from '../pages/metrics'; import { MetricDetail } from '../pages/metrics/metric_detail'; -import { ClientPluginDeps } from '../types'; +import { InfraClientStartDeps } from '../types'; import { createApolloClient } from '../utils/apollo_client'; import { RedirectWithQueryParams } from '../utils/redirect_with_query_params'; import { CommonInfraProviders, CoreProviders } from './common_providers'; @@ -24,7 +24,7 @@ import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, - plugins: ClientPluginDeps, + plugins: InfraClientStartDeps, { element, history }: AppMountParameters ) => { const apolloClient = createApolloClient(core.http.fetch); @@ -45,7 +45,7 @@ const MetricsApp: React.FC<{ apolloClient: ApolloClient<{}>; core: CoreStart; history: History; - plugins: ClientPluginDeps; + plugins: InfraClientStartDeps; }> = ({ apolloClient, core, history, plugins }) => { const uiCapabilities = core.application.capabilities; diff --git a/x-pack/plugins/infra/public/index.ts b/x-pack/plugins/infra/public/index.ts index 8f2d37fa1daa9..cadf9a4837866 100644 --- a/x-pack/plugins/infra/public/index.ts +++ b/x-pack/plugins/infra/public/index.ts @@ -5,14 +5,19 @@ */ import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; -import { ClientSetup, ClientStart, Plugin } from './plugin'; -import { ClientPluginsSetup, ClientPluginsStart } from './types'; +import { Plugin } from './plugin'; +import { + InfraClientSetupExports, + InfraClientStartExports, + InfraClientSetupDeps, + InfraClientStartDeps, +} from './types'; export const plugin: PluginInitializer< - ClientSetup, - ClientStart, - ClientPluginsSetup, - ClientPluginsStart + InfraClientSetupExports, + InfraClientStartExports, + InfraClientSetupDeps, + InfraClientStartDeps > = (context: PluginInitializerContext) => { return new Plugin(context); }; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 496e788efc060..1b28945320bb6 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -4,35 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin as PluginClass, - PluginInitializerContext, -} from 'kibana/public'; +import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; import { registerStartSingleton } from './legacy_singletons'; import { registerFeatures } from './register_feature'; -import { ClientPluginsSetup, ClientPluginsStart } from './types'; - -export type ClientSetup = void; -export type ClientStart = void; +import { + InfraClientSetupDeps, + InfraClientStartDeps, + InfraClientCoreSetup, + InfraClientCoreStart, + InfraClientPluginClass, +} from './types'; +import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; -export class Plugin - implements PluginClass { +export class Plugin implements InfraClientPluginClass { constructor(_context: PluginInitializerContext) {} - setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { + setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) { registerFeatures(pluginsSetup.home); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createInventoryMetricAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType()); + if (pluginsSetup.observability) { + pluginsSetup.observability.dashboard.register({ + appName: 'infra_logs', + hasData: getLogsHasDataFetcher(core.getStartServices), + fetchData: getLogsOverviewDataFetcher(core.getStartServices), + }); + } + core.application.register({ id: 'logs', title: i18n.translate('xpack.infra.logs.pluginTitle', { @@ -43,20 +48,11 @@ export class Plugin appRoute: '/app/logs', category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { + // mount callback should not use setup dependencies, get start dependencies instead const [coreStart, pluginsStart] = await core.getStartServices(); const { renderApp } = await import('./apps/logs_app'); - return renderApp( - coreStart, - { - data: pluginsStart.data, - dataEnhanced: pluginsSetup.dataEnhanced, - home: pluginsSetup.home, - triggers_actions_ui: pluginsStart.triggers_actions_ui, - usageCollection: pluginsSetup.usageCollection, - }, - params - ); + return renderApp(coreStart, pluginsStart, params); }, }); @@ -70,20 +66,11 @@ export class Plugin appRoute: '/app/metrics', category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { + // mount callback should not use setup dependencies, get start dependencies instead const [coreStart, pluginsStart] = await core.getStartServices(); const { renderApp } = await import('./apps/metrics_app'); - return renderApp( - coreStart, - { - data: pluginsStart.data, - dataEnhanced: pluginsSetup.dataEnhanced, - home: pluginsSetup.home, - triggers_actions_ui: pluginsStart.triggers_actions_ui, - usageCollection: pluginsSetup.usageCollection, - }, - params - ); + return renderApp(coreStart, pluginsStart, params); }, }); @@ -102,7 +89,9 @@ export class Plugin }); } - start(core: CoreStart, _plugins: ClientPluginsStart) { + start(core: InfraClientCoreStart, _plugins: InfraClientStartDeps) { registerStartSingleton(core); } + + stop() {} } diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 8181da3301c92..357f07265ac6e 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -4,22 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { + UsageCollectionSetup, + UsageCollectionStart, +} from '../../../../src/plugins/usage_collection/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; -import { DataEnhancedSetup } from '../../data_enhanced/public'; +import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; +import { ObservabilityPluginSetup, ObservabilityPluginStart } from '../../observability/public'; -export interface ClientPluginsSetup { +// Our own setup and start contract values +export type InfraClientSetupExports = void; +export type InfraClientStartExports = void; + +export interface InfraClientSetupDeps { dataEnhanced: DataEnhancedSetup; home: HomePublicPluginSetup; + observability: ObservabilityPluginSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; } -export interface ClientPluginsStart { +export interface InfraClientStartDeps { data: DataPublicPluginStart; + dataEnhanced: DataEnhancedStart; + observability: ObservabilityPluginStart; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; + usageCollection: UsageCollectionStart; } -export type ClientPluginDeps = ClientPluginsSetup & ClientPluginsStart; +export type InfraClientCoreSetup = CoreSetup; +export type InfraClientCoreStart = CoreStart; +export type InfraClientPluginClass = PluginClass< + InfraClientSetupExports, + InfraClientStartExports, + InfraClientSetupDeps, + InfraClientStartDeps +>; diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts new file mode 100644 index 0000000000000..46a0edf75b756 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraClientCoreSetup } from '../types'; +import { LogsFetchDataResponse } from '../../../observability/public'; + +export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) { + return async () => { + // if you need the data plugin, this is how you get it + // const [, startPlugins] = await getStartServices(); + // const { data } = startPlugins; + + // if you need a core dep, we need to pass in more than just getStartServices + + // perform query + return true; + }; +} + +export function getLogsOverviewDataFetcher( + getStartServices: InfraClientCoreSetup['getStartServices'] +) { + return async (): Promise => { + // if you need the data plugin, this is how you get it + // const [, startPlugins] = await getStartServices(); + // const { data } = startPlugins; + + // if you need a core dep, we need to pass in more than just getStartServices + + // perform query + return { + title: 'Log rate', + appLink: 'TBD', // TODO: what format should this be in, relative I assume? + stats: { + nginx: { + type: 'number', + label: 'nginx', + value: 345341, + }, + 'elasticsearch.audit': { + type: 'number', + label: 'elasticsearch.audit', + value: 164929, + }, + 'haproxy.log': { + type: 'number', + label: 'haproxy.log', + value: 51101, + }, + }, + // Note: My understanding is that these series coordinates will be + // combined into objects that look like: + // { x: timestamp, y: value, g: label (e.g. nginx) } + // so they fit the stacked bar chart API + // https://elastic.github.io/elastic-charts/?path=/story/bar-chart--stacked-with-axis-and-legend + series: { + nginx: { + label: 'nginx', + coordinates: [ + { x: 1593000000000, y: 10014 }, + { x: 1593000900000, y: 12827 }, + { x: 1593001800000, y: 2946 }, + { x: 1593002700000, y: 14298 }, + { x: 1593003600000, y: 4096 }, + ], + }, + 'elasticsearch.audit': { + label: 'elasticsearch.audit', + coordinates: [ + { x: 1593000000000, y: 5676 }, + { x: 1593000900000, y: 6783 }, + { x: 1593001800000, y: 2394 }, + { x: 1593002700000, y: 4554 }, + { x: 1593003600000, y: 5659 }, + ], + }, + 'haproxy.log': { + label: 'haproxy.log', + coordinates: [ + { x: 1593000000000, y: 9085 }, + { x: 1593000900000, y: 9002 }, + { x: 1593001800000, y: 3940 }, + { x: 1593002700000, y: 5451 }, + { x: 1593003600000, y: 9133 }, + ], + }, + }, + }; + }; +} diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 65f2c52a4e320..39e702a332a8e 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -4,29 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ObservabilityFetchDataResponse, FetchDataResponse } from './typings/fetch_data_response'; +import { DataHandler } from './typings/fetch_overview_data'; import { ObservabilityApp } from '../typings/common'; -export interface FetchDataParams { - // The start timestamp in milliseconds of the queried time interval - startTime: string; - // The end timestamp in milliseconds of the queried time interval - endTime: string; - // The aggregation bucket size in milliseconds if applicable to the data source - bucketSize: string; -} - -export type FetchData = ( - fetchDataParams: FetchDataParams -) => Promise; - -export type HasData = () => Promise; - -interface DataHandler { - fetchData: FetchData; - hasData: HasData; -} - const dataHandlers: Partial> = {}; export function registerDataHandler({ diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index fcb569f535d76..d2f1d246f79ec 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -5,16 +5,16 @@ */ import { PluginInitializerContext, PluginInitializer } from 'kibana/public'; -import { Plugin, ObservabilityPluginSetup } from './plugin'; +import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin'; -export const plugin: PluginInitializer = ( +export { ObservabilityPluginSetup, ObservabilityPluginStart }; + +export const plugin: PluginInitializer = ( context: PluginInitializerContext ) => { return new Plugin(context); }; -export { ObservabilityPluginSetup }; - export * from './components/action_menu'; export { diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index c20e8c7b75d49..bbda1026606f1 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -16,7 +16,9 @@ export interface ObservabilityPluginSetup { dashboard: { register: typeof registerDataHandler }; } -export class Plugin implements PluginClass { +export type ObservabilityPluginStart = void; + +export class Plugin implements PluginClass { constructor(context: PluginInitializerContext) {} public setup(core: CoreSetup) { diff --git a/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts similarity index 68% rename from x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts rename to x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index 06e86d1096cfc..e65d1779520cf 100644 --- a/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ObservabilityApp } from '../../../typings/common'; + interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; label: string; @@ -22,6 +24,26 @@ interface Series { color?: string; } +export interface FetchDataParams { + // The start timestamp in milliseconds of the queried time interval + startTime: string; + // The end timestamp in milliseconds of the queried time interval + endTime: string; + // The aggregation bucket size in milliseconds if applicable to the data source + bucketSize: string; +} + +export type FetchData = ( + fetchDataParams: FetchDataParams +) => Promise; + +export type HasData = () => Promise; + +export interface DataHandler { + fetchData: FetchData; + hasData: HasData; +} + export interface FetchDataResponse { title: string; appLink: string; diff --git a/x-pack/plugins/observability/public/typings/index.ts b/x-pack/plugins/observability/public/typings/index.ts index 3da2febc73efd..5cc2c613881df 100644 --- a/x-pack/plugins/observability/public/typings/index.ts +++ b/x-pack/plugins/observability/public/typings/index.ts @@ -6,3 +6,4 @@ export * from './eui_draggable'; export * from './eui_styled_components'; +export * from './fetch_overview_data';