From afbf1a983aadd60621ccc3689876211688f3fa1d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Nov 2020 15:49:22 +0100 Subject: [PATCH 1/7] [APM] Errors table for service overview (#83065) --- .../runtime_types/to_number_rt/index.ts | 16 ++ .../ServiceList/ServiceListMetric.tsx | 33 +-- .../components/app/service_overview/index.tsx | 34 +-- .../fetch_wrapper.tsx | 30 ++ .../service_overview_errors_table/index.tsx | 266 ++++++++++++++++++ .../service_overview/table_link_flex_item.tsx | 14 + .../{SparkPlot => spark_plot}/index.tsx | 0 .../spark_plot_with_value_label/index.tsx | 57 ++++ .../apm/server/lib/errors/get_error_groups.ts | 4 +- .../apm/server/lib/helpers/get_error_name.ts | 11 + .../get_service_error_groups/index.ts | 177 ++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + x-pack/plugins/apm/server/routes/services.ts | 44 +++ .../monitoring/workload_statistics.test.ts | 32 ++- .../apm_api_integration/basic/tests/index.ts | 4 + .../tests/service_overview/error_groups.ts | 220 +++++++++++++++ .../typings/elasticsearch/aggregations.d.ts | 2 + 17 files changed, 883 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx rename x-pack/plugins/apm/public/components/shared/charts/{SparkPlot => spark_plot}/index.tsx (100%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/helpers/get_error_name.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts diff --git a/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts new file mode 100644 index 00000000000000..0fe8181c114052 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; + +export const toNumberRt = new t.Type( + 'ToNumber', + t.any.is, + (input, context) => { + const number = Number(input); + return !isNaN(number) ? t.success(number) : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index c94c94d4a0b72c..716fed7775f7b4 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -3,15 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; - import React from 'react'; -import { useTheme } from '../../../../hooks/useTheme'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; -import { SparkPlot } from '../../../shared/charts/SparkPlot'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; export function ServiceListMetric({ color, @@ -22,28 +16,17 @@ export function ServiceListMetric({ series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; }) { - const theme = useTheme(); - const { urlParams: { start, end }, } = useUrlParams(); - const colorValue = theme.eui[color]; - return ( - - - - - - {valueLabel} - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 016ee3daf6b510..ee77157fe4eb36 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -12,9 +12,10 @@ import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; -import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; +import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { TableLinkFlexItem } from './table_link_flex_item'; const rowHeight = 310; const latencyChartRowHeight = 230; @@ -27,12 +28,6 @@ const LatencyChartRow = styled(EuiFlexItem)` height: ${latencyChartRowHeight}px; `; -const TableLinkFlexItem = styled(EuiFlexItem)` - & > a { - text-align: right; - } -`; - interface ServiceOverviewProps { agentName?: string; serviceName: string; @@ -130,30 +125,7 @@ export function ServiceOverview({ )} - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableTitle', - { - defaultMessage: 'Errors', - } - )} -

-
-
- - - {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableLinkText', - { - defaultMessage: 'View errors', - } - )} - - -
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx new file mode 100644 index 00000000000000..4c8d368811a0c1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; + +export function FetchWrapper({ + hasData, + status, + children, +}: { + hasData: boolean; + status: FETCH_STATUS; + children: React.ReactNode; +}) { + if (status === FETCH_STATUS.FAILURE) { + return ; + } + + if (!hasData && status !== FETCH_STATUS.SUCCESS) { + return ; + } + + return <>{children}; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx new file mode 100644 index 00000000000000..a5a002cf3aca49 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FetchWrapper } from './fetch_wrapper'; + +interface Props { + serviceName: string; +} + +interface ErrorGroupItem { + name: string; + last_seen: number; + group_id: string; + occurrences: { + value: number; + timeseries: Array<{ x: number; y: number }> | null; + }; +} + +type SortDirection = 'asc' | 'desc'; +type SortField = 'name' | 'last_seen' | 'occurrences'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'occurrences' as const, +}; + +const ErrorDetailLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledErrorDetailLink = styled(ErrorDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewErrorsTable({ serviceName }: Props) { + const { + urlParams: { start, end }, + uiFilters, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + + + {name} + + + + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 8), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences }) => { + return ( + + ); + }, + }, + ]; + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/error_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.error_groups, + totalItemCount: response.total_error_groups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + start, + end, + serviceName, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + return ( + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.errorsTableTitle', { + defaultMessage: 'Errors', + })} +

+
+
+ + + {i18n.translate('xpack.apm.serviceOverview.errorsTableLinkText', { + defaultMessage: 'View errors', + })} + + +
+
+ + + { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx new file mode 100644 index 00000000000000..35df003af380d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx new file mode 100644 index 00000000000000..e2bb42fddb33bb --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../../hooks/useTheme'; +import { getEmptySeries } from '../../CustomPlot/getEmptySeries'; +import { SparkPlot } from '../'; + +type Color = + | 'euiColorVis0' + | 'euiColorVis1' + | 'euiColorVis2' + | 'euiColorVis3' + | 'euiColorVis4' + | 'euiColorVis5' + | 'euiColorVis6' + | 'euiColorVis7' + | 'euiColorVis8' + | 'euiColorVis9'; + +export function SparkPlotWithValueLabel({ + start, + end, + color, + series, + valueLabel, +}: { + start: number; + end: number; + color: Color; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index d734a1395fc5e4..97c03924538c80 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -16,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; +import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -93,8 +94,7 @@ export async function getErrorGroups({ // this is an exception rather than the rule so the ES type does not account for this. const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => { const source = bucket.sample.hits.hits[0]._source; - const message = - source.error.log?.message || source.error.exception?.[0]?.message; + const message = getErrorName(source); return { message, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts new file mode 100644 index 00000000000000..dbc69592a4f8e8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APMError } from '../../../typings/es_schemas/ui/apm_error'; + +export function getErrorName({ error }: APMError) { + return error.log?.message || error.exception?.[0]?.message; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts new file mode 100644 index 00000000000000..99d978116180b7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -0,0 +1,177 @@ +/* + * 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 { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getErrorName } from '../../helpers/get_error_name'; + +export type ServiceErrorGroupItem = ValuesType< + PromiseReturnType +>; + +export async function getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: 'name' | 'last_seen' | 'occurrences'; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize(start, end, numBuckets); + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: { + value: bucket.doc_count, + }, + })) ?? []; + + // Sort error groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedErrorGroups = orderBy( + errorGroups, + (group) => { + if (sortField === 'occurrences') { + return group.occurrences.value; + } + return group[sortField]; + }, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + const sortedErrorGroupIds = sortedAndSlicedErrorGroups.map( + (group) => group.group_id + ); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + return { + total_error_groups: errorGroups.length, + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: sortedAndSlicedErrorGroups.map((errorGroup) => ({ + ...errorGroup, + occurrences: { + ...errorGroup.occurrences, + timeseries: + timeseriesResponse.aggregations?.error_groups.buckets + .find((bucket) => bucket.key === errorGroup.group_id) + ?.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count, + })) ?? null, + }, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2fbe404a70d829..34551c35ee2342 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -21,6 +21,7 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, + serviceErrorGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -115,6 +116,7 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 590b6c49d71bf8..ada1674d4555db 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -17,6 +17,8 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -195,3 +197,45 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ }); }, })); + +export const serviceErrorGroupsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/error_groups', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('last_seen'), + t.literal('occurrences'), + t.literal('name'), + ]), + }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + }); + }, +})); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index d1c5256c81c63d..c2e62b6e1898b7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -46,9 +46,13 @@ describe('Workload Statistics Aggregator', () => { aggregations: { taskType: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, schedule: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, idleTasks: { doc_count: 0, @@ -158,6 +162,8 @@ describe('Workload Statistics Aggregator', () => { }, aggregations: { schedule: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: '3600s', @@ -174,11 +180,15 @@ describe('Workload Statistics Aggregator', () => { ], }, taskType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'actions_telemetry', doc_count: 2, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -191,6 +201,8 @@ describe('Workload Statistics Aggregator', () => { key: 'alerting_telemetry', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -203,6 +215,8 @@ describe('Workload Statistics Aggregator', () => { key: 'session_cleanup', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -608,6 +622,7 @@ describe('padBuckets', () => { key: 1601668047000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -617,6 +632,7 @@ describe('padBuckets', () => { key: 1601668050000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -626,6 +642,7 @@ describe('padBuckets', () => { key: 1601668053000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -635,6 +652,7 @@ describe('padBuckets', () => { key: 1601668056000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -644,6 +662,7 @@ describe('padBuckets', () => { key: 1601668059000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -653,6 +672,7 @@ describe('padBuckets', () => { key: 1601668062000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -678,13 +698,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -707,13 +727,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -796,7 +816,7 @@ function mockHistogram( key_as_string: key.toISOString(), key: key.getTime(), doc_count: count, - interval: { buckets: [] }, + interval: { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, }); } return histogramBuckets; @@ -806,6 +826,8 @@ function mockHistogram( key: number; doc_count: number; interval: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array<{ key: string; doc_count: number; 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 df3e60d79aca57..39dd721c7067e9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -25,6 +25,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./services/transaction_types')); }); + describe('Service overview', function () { + loadTestFile(require.resolve('./service_overview/error_groups')); + }); + describe('Settings', function () { loadTestFile(require.resolve('./settings/custom_link')); loadTestFile(require.resolve('./settings/agent_configuration')); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts new file mode 100644 index 00000000000000..b699a30d40418c --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -0,0 +1,220 @@ +/* + * 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 qs from 'querystring'; +import { pick, uniqBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview error groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + total_error_groups: 0, + error_groups: [], + is_aggregation_accurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); + + expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "java.io.IOException: Connection reset by peer", + "Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst369_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) + .toMatchInline(` + Array [ + 8, + 2, + 1, + 1, + 1, + ] + `); + + const firstItem = response.body.error_groups[0]; + + expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) + .toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1601391561523, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "occurrences": Object { + "value": 8, + }, + } + `); + + expectSnapshot( + firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`7`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + const ascendingOccurrences = ascendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'last_seen', + })}` + ); + + expect(response.status).to.be(200); + + const dates = response.body.error_groups.map((group: any) => group.last_seen); + + expect(dates).to.eql(dates.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.total_error_groups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + return prevItems.concat(thisPage.body.error_groups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'group_id').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 29c78e93831751..bc9ed447c8717f 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -204,6 +204,8 @@ type SubAggregationResponseOf< interface AggregationResponsePart { terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array< { doc_count: number; From 58ad7ecd5adae6db8b91f648444ca045f3b5404e Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Thu, 12 Nov 2020 15:53:53 +0100 Subject: [PATCH 2/7] Btsymbala/registered av (#81910) * Moved out type for OperatingSystem and moved OS translations one level higher. * Changed the translation to be consistent between trusted apps and policy. * Unified translations of OS types between trusted apps and policy. * Removed unused types. * Added registered AV form section. * Changed the property structure to match the format expected by endpoint. * Fixed the visual alignment of titles in the form and added responsiveness. * Updated snapshots. * Moved out type for OperatingSystem and moved OS translations one level higher. * Added config form heading component. * Cleaned up translations. * Fixed type error with initialization. * Fixed error in trusted app creation form test. * Removed the guard for now in favour of better initialization. * Fixed the store test. * Fixing functional test data. * Added functional test config option to account for a custom header within security app. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/models/policy_config.ts | 3 + .../common/endpoint/types/index.ts | 9 +- .../common/endpoint/types/os.ts | 10 +++ .../common/endpoint/types/trusted_apps.ts | 5 +- .../public/management/common/translations.ts | 14 +++ .../policy/store/policy_details/action.ts | 10 ++- .../policy/store/policy_details/index.test.ts | 3 + .../policy/store/policy_details/reducer.ts | 45 +++++++++- .../policy/store/policy_details/selectors.ts | 5 ++ .../public/management/pages/policy/types.ts | 62 ------------- .../components/config_form/index.stories.tsx | 60 +++++++++++++ .../view/components/config_form/index.tsx | 84 +++++++++++++++++ .../pages/policy/view/policy_details.tsx | 3 + .../antivirus_registration/index.tsx | 64 +++++++++++++ .../policy/view/policy_forms/config_form.tsx | 89 ------------------- .../policy/view/policy_forms/events/linux.tsx | 45 ++++------ .../policy/view/policy_forms/events/mac.tsx | 45 ++++------ .../view/policy_forms/events/translations.ts | 28 ++++++ .../view/policy_forms/events/windows.tsx | 45 ++++------ .../view/policy_forms/protections/malware.tsx | 39 ++++---- .../create_trusted_app_form.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../__snapshots__/index.test.tsx.snap | 18 ++-- .../__snapshots__/index.test.tsx.snap | 56 ++++++------ .../pages/trusted_apps/view/translations.ts | 14 +-- .../translations/translations/ja-JP.json | 7 -- .../translations/translations/zh-CN.json | 7 -- .../apps/endpoint/policy_details.ts | 3 + .../test/security_solution_endpoint/config.ts | 3 + 29 files changed, 447 insertions(+), 335 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/os.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 3250e048edad21..890def5b63d4a7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -33,6 +33,9 @@ export const factory = (): PolicyConfig => { logging: { file: 'info', }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1d64578a6a7f11..673d04c856935f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -8,6 +8,7 @@ import { ApplicationStart } from 'kibana/public'; import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; +export * from './os'; export * from './trusted_apps'; /** @@ -880,6 +881,9 @@ export interface PolicyConfig { enabled: boolean; }; }; + antivirus_registration: { + enabled: boolean; + }; }; mac: { advanced?: {}; @@ -919,7 +923,10 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick< + PolicyConfig['windows'], + 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced' + >; /** * Mac-specific policy configuration that is supported via the UI */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts new file mode 100644 index 00000000000000..b9afbd63ecd54a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type Linux = 'linux'; +export type MacOS = 'macos'; +export type Windows = 'windows'; +export type OperatingSystem = Linux | MacOS | Windows; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3568136dd0e7b9..79d66443bc8f13 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -11,6 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; +import { Linux, MacOS, Windows } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -51,11 +52,11 @@ export type NewTrustedApp = { description?: string; } & ( | { - os: 'linux' | 'macos'; + os: Linux | MacOS; entries: MacosLinuxConditionEntry[]; } | { - os: 'windows'; + os: Windows; entries: WindowsConditionEntry[]; } ); diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index d24eb1bd315fa7..415658c1fd6afd 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '../../../common/endpoint/types'; + export const ENDPOINTS_TAB = i18n.translate('xpack.securitySolution.endpointsTab', { defaultMessage: 'Endpoints', }); @@ -21,3 +23,15 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administration.list.beta', { defaultMessage: 'Beta', }); + +export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = { + windows: i18n.translate('xpack.securitySolution.administration.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.administration.os.macos', { + defaultMessage: 'Mac', + }), + linux: i18n.translate('xpack.securitySolution.administration.os.linux', { + defaultMessage: 'Linux', + }), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts index f5a219bce4a6bd..bda408cd00e75e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts @@ -31,6 +31,13 @@ interface UserChangedPolicyConfig { }; } +interface UserChangedAntivirusRegistration { + type: 'userChangedAntivirusRegistration'; + payload: { + enabled: boolean; + }; +} + interface ServerReturnedPolicyDetailsAgentSummaryData { type: 'serverReturnedPolicyDetailsAgentSummaryData'; payload: { @@ -62,4 +69,5 @@ export type PolicyDetailsAction = | ServerReturnedPolicyDetailsUpdateFailure | ServerReturnedUpdatedPolicyDetailsData | ServerFailedToReturnPolicyDetailsData - | UserChangedPolicyConfig; + | UserChangedPolicyConfig + | UserChangedAntivirusRegistration; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 89ba05547f4479..69c2afbd019607 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -245,6 +245,9 @@ describe('policy details: ', () => { }, }, logging: { file: 'info' }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { process: true, file: true, network: true }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index 43a6ad2c585b43..bcdc7ba2089c64 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -4,11 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; -import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { + Immutable, + PolicyConfig, + UIPolicyConfig, + PolicyData, +} from '../../../../../../common/endpoint/types'; import { ImmutableReducer } from '../../../../../common/store'; import { AppAction } from '../../../../../common/store/actions'; import { PolicyDetailsState } from '../../types'; +const updatePolicyConfigInPolicyData = ( + policyData: Immutable, + policyConfig: Immutable +) => ({ + ...policyData, + inputs: policyData.inputs.map((input) => ({ + ...input, + config: input.config && { + ...input.config, + policy: { + ...input.config.policy, + value: policyConfig, + }, + }, + })), +}); + /** * Return a fresh copy of initial state, since we mutate state in the reducer. */ @@ -126,5 +148,26 @@ export const policyDetailsReducer: ImmutableReducer UIPolicyConfig = createSel events: windows.events, malware: windows.malware, popup: windows.popup, + antivirus_registration: windows.antivirus_registration, }, mac: { advanced: mac.advanced, @@ -122,6 +123,10 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); +export const isAntivirusRegistrationEnabled = createSelector(policyConfig, (uiPolicyConfig) => { + return uiPolicyConfig.windows.antivirus_registration.enabled; +}); + /** Returns the total number of possible windows eventing configurations */ export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 152caff3714b0a..3926ad2220e35d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -76,68 +76,6 @@ export interface PolicyListUrlSearchParams { page_size: number; } -/** - * Endpoint Policy configuration - */ -export interface PolicyConfig { - windows: { - events: { - dll_and_driver_load: boolean; - dns: boolean; - file: boolean; - network: boolean; - process: boolean; - registry: boolean; - security: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - mac: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - linux: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; -} - -interface PolicyConfigAdvancedOptions { - elasticsearch: { - indices: { - control: string; - event: string; - logging: string; - }; - kernel: { - connect: boolean; - process: boolean; - }; - }; -} - export enum OS { windows = 'windows', mac = 'mac', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx new file mode 100644 index 00000000000000..4f288af393b7c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { ConfigForm } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('PolicyDetails/ConfigForm', module) + .add('One OS', () => { + return ( + + {'Some content'} + + ); + }) + .add('Multiple OSs', () => { + return ( + + {'Some content'} + + ); + }) + .add('Complex content', () => { + return ( + + + {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + + 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + + 'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + + 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' + + 'deserunt mollit anim id est laborum.'} + + + {}} /> + + {}} /> + {}} /> + {}} /> + + ); + }) + .add('Right corner content', () => { + const toggle = {}} />; + + return ( + + {'Some content'} + + ); + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx new file mode 100644 index 00000000000000..30c35de9b907f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactNode, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiShowFor, + EuiPanel, +} from '@elastic/eui'; + +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OS_TITLES } from '../../../../../common/translations'; + +const TITLES = { + type: i18n.translate('xpack.securitySolution.endpoint.policyDetailType', { + defaultMessage: 'Type', + }), + os: i18n.translate('xpack.securitySolution.endpoint.policyDetailOS', { + defaultMessage: 'Operating System', + }), +}; + +interface ConfigFormProps { + /** + * A subtitle for this component. + **/ + type: string; + /** + * Types of supported operating systems. + */ + supportedOss: OperatingSystem[]; + dataTestSubj?: string; + /** React Node to be put on the right corner of the card */ + rightCorner?: ReactNode; +} + +export const ConfigFormHeading: FC = memo(({ children }) => ( + +
{children}
+
+)); + +ConfigFormHeading.displayName = 'ConfigFormHeading'; + +export const ConfigForm: FC = memo( + ({ type, supportedOss, dataTestSubj, rightCorner, children }) => ( + + + + {TITLES.type} + {type} + + + {TITLES.os} + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + + + + {rightCorner} + + + + + {rightCorner} + + + + + + {children} + + ) +); + +ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 8fc5de48f36db2..9c11bc6f5a4d13 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,6 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -251,6 +252,8 @@ export const PolicyDetails = React.memo(() => { + + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx new file mode 100644 index 00000000000000..8d1ac29c8ce1e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { ConfigForm } from '../../components/config_form'; + +export const AntivirusRegistrationForm = memo(() => { + const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); + const dispatch = useDispatch(); + + const handleSwitchChange = useCallback( + (event) => + dispatch({ + type: 'userChangedAntivirusRegistration', + payload: { + enabled: event.target.checked, + }, + }), + [dispatch] + ); + + return ( + + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', + } + )} + + + + + ); +}); + +AntivirusRegistrationForm.displayName = 'AntivirusRegistrationForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx deleted file mode 100644 index 8e3c4138efb36e..00000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiHorizontalRule, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; - -const PolicyDetailCard = styled.div` - .policyDetailTitleOS { - flex-grow: 2; - } - .policyDetailTitleFlexItem { - margin: 0; - } -`; -export const ConfigForm: React.FC<{ - /** - * A subtitle for this component. - **/ - type: string; - /** - * Types of supported operating systems. - */ - supportedOss: React.ReactNode; - children: React.ReactNode; - dataTestSubj: string; - /** React Node to be put on the right corner of the card */ - rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner }) => { - const typeTitle = useMemo(() => { - return ( - - - - -
- -
-
-
- - {type} - -
- - - -
- -
-
-
- - {supportedOss} - -
- {rightCorner} -
- ); - }, [rightCorner, supportedOss, type]); - - return ( - - - - {children} - - - ); -}); - -ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 66126adb7a4e1d..b43f93f1a1e2be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); @@ -59,14 +63,7 @@ export const LinuxEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const LinuxEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index dc70fc0ba0f4fd..fbbe50fbec1b03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); @@ -59,14 +63,7 @@ export const MacEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const MacEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts new file mode 100644 index 00000000000000..3b48b7969a8ced --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EVENTS_HEADING = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', + { + defaultMessage: 'Events', + } +); + +export const EVENTS_FORM_TYPE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.eventCollection', + { + defaultMessage: 'Event Collection', + } +); + +export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { + return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected, total }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index 5acdf67922a3a6..f7b1a8e901ed2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); @@ -99,14 +103,7 @@ export const WindowsEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -125,28 +122,16 @@ export const WindowsEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index b61dee52697370..7259b2ec19ee2a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiRadio, EuiSwitch, - EuiTitle, EuiText, EuiSpacer, EuiTextArea, @@ -18,15 +19,13 @@ import { EuiCallOut, EuiCheckbox, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; @@ -200,14 +199,12 @@ export const MalwareProtections = React.memo(() => { const radioButtons = useMemo(() => { return ( <> - -
- -
-
+ + + {radios.map((radio) => { @@ -221,14 +218,12 @@ export const MalwareProtections = React.memo(() => { })} - -
- -
-
+ + + { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', { defaultMessage: 'Malware', })} - supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.windowsAndMac', { - defaultMessage: 'Windows, Mac', - })} + supportedOss={['windows', 'macos']} dataTestSubj="malwareProtectionsForm" rightCorner={protectionSwitch} > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 211fc9ec3371e1..4bac9164e1d621 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -118,7 +118,7 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Mac OS', 'Windows', 'Linux']); + expect(options).toEqual(['Mac', 'Windows', 'Linux']); }); it('should show Description as optional', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index a94e6287a4f58b..a47558257420c5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -17,7 +17,7 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` value={ } /> @@ -112,7 +112,7 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` value={ } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index c82b9cac8ab1fc..6d45059099f8d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -412,7 +412,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1168,7 +1168,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1924,7 +1924,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -3222,7 +3222,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -3978,7 +3978,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -4734,7 +4734,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -5990,7 +5990,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -6746,7 +6746,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -7502,7 +7502,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 2797c433b8236c..d0459871d48811 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -1061,7 +1061,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1448,7 +1448,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1835,7 +1835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2222,7 +2222,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2609,7 +2609,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2996,7 +2996,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -3383,7 +3383,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4001,7 +4001,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4388,7 +4388,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4775,7 +4775,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5162,7 +5162,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5549,7 +5549,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5936,7 +5936,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -6323,7 +6323,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7099,7 +7099,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7486,7 +7486,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7873,7 +7873,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8260,7 +8260,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8647,7 +8647,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9034,7 +9034,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9421,7 +9421,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10039,7 +10039,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10426,7 +10426,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10813,7 +10813,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11200,7 +11200,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11587,7 +11587,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11974,7 +11974,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -12361,7 +12361,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b2f62c2f1da4eb..4c2b3f0e59ccb4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -11,23 +11,13 @@ import { WindowsConditionEntry, } from '../../../../../common/endpoint/types'; +export { OS_TITLES } from '../../../common/translations'; + export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', }); -export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { - windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { - defaultMessage: 'Windows', - }), - macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { - defaultMessage: 'Mac OS', - }), - linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { - defaultMessage: 'Linux', - }), -}; - type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 852c3ffd9bfd67..912cb01d458e2e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17498,8 +17498,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "{detectionRulesLink}を表示します。事前構築済みルールは、[検出ルール]ページで「Elastic」というタグが付けられています。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total}件のイベント収集が有効です", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "マルウェア", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "マルウェア保護{mode, select, true {有効} false {無効}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17514,8 +17512,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "オペレーティングシステム", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "エラー", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "オフライン", @@ -18466,9 +18462,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者", "xpack.securitySolution.trustedapps.trustedapp.description": "説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 461b93e2e081d7..8ae964d9ee7d09 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17516,8 +17516,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "请查看{detectionRulesLink}。在“检测规则”页面上,预置规则标记有“Elastic”。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 事件收集已启用", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "恶意软件", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "恶意软件防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17533,8 +17531,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "操作系统", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "错误", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "脱机", @@ -18485,9 +18481,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者", "xpack.securitySolution.trustedapps.trustedapp.description": "描述", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 15c26a1b9374d6..f032416d2e7bb7 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -221,6 +221,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { message: 'Elastic Security { action } { filename }', }, }, + antivirus_registration: { + enabled: false, + }, }, }, streams: [], diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 9499c235a5f0d5..f3cb4a5812a5cb 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -43,5 +43,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...getRegistryUrlAsArray(), ], }, + layout: { + fixedHeaderHeight: 200, + }, }; } From eaa65535edf5ad7bb64d50373bc7587ca18d1d7f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 12 Nov 2020 15:54:55 +0100 Subject: [PATCH 3/7] Use saved object references for dashboard drilldowns (#82602) --- ...ver.embeddablesetup.getattributeservice.md | 11 - ...ugins-embeddable-server.embeddablesetup.md | 3 +- .../embeddable/embeddable_references.test.ts | 87 ++++++ .../embeddable/embeddable_references.ts | 82 +++++ ...embeddable_saved_object_converters.test.ts | 26 +- .../embeddable_saved_object_converters.ts | 7 +- .../saved_dashboard_references.test.ts | 164 +++++----- .../saved_dashboard_references.ts | 75 ++++- src/plugins/dashboard/common/types.ts | 17 ++ .../application/dashboard_app_controller.tsx | 2 +- .../application/dashboard_state_manager.ts | 2 +- .../public/application/embeddable/types.ts | 12 +- src/plugins/dashboard/public/plugin.tsx | 1 + .../public/saved_dashboards/index.ts | 2 +- .../saved_dashboards/saved_dashboard.ts | 22 +- .../saved_dashboards/saved_dashboards.ts | 10 +- src/plugins/dashboard/public/types.ts | 10 +- src/plugins/dashboard/server/plugin.ts | 20 +- .../server/saved_objects/dashboard.ts | 15 +- .../dashboard_migrations.test.ts | 55 +++- .../saved_objects/dashboard_migrations.ts | 62 +++- .../dashboard/server/saved_objects/index.ts | 2 +- .../saved_objects/migrations_730.test.ts | 6 +- src/plugins/embeddable/common/index.ts | 21 ++ src/plugins/embeddable/common/lib/index.ts | 1 + .../lib}/saved_object_embeddable.ts | 2 +- src/plugins/embeddable/common/mocks.ts | 31 ++ src/plugins/embeddable/common/types.ts | 15 +- .../public/lib/containers/container.ts | 2 +- .../public/lib/containers/i_container.ts | 12 +- .../public/lib/embeddables/index.ts | 2 +- src/plugins/embeddable/server/mocks.ts | 30 ++ src/plugins/embeddable/server/plugin.ts | 17 +- src/plugins/embeddable/server/server.api.md | 5 +- .../dashboard_drilldown/constants.ts | 14 + ...hboard_drilldown_persistable_state.test.ts | 48 +++ .../dashboard_drilldown_persistable_state.ts | 75 +++++ .../drilldowns/dashboard_drilldown/index.ts | 9 + .../drilldowns/dashboard_drilldown/types.ts | 12 + .../common/drilldowns/index.ts | 7 + .../dashboard_enhanced/common/index.ts | 7 + x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- .../abstract_dashboard_drilldown.tsx | 8 +- .../abstract_dashboard_drilldown/types.ts | 8 +- ...embeddable_to_dashboard_drilldown.test.tsx | 6 + .../embeddable_to_dashboard_drilldown.tsx | 5 + .../dashboard_enhanced/server/index.ts | 19 ++ .../dashboard_enhanced/server/plugin.ts | 44 +++ .../ui_actions_enhanced/common/index.ts | 7 + .../server/dynamic_action_enhancement.ts | 4 +- .../ui_actions_enhanced/server/index.ts | 6 +- .../ui_actions_enhanced/server/plugin.ts | 4 +- .../dashboard_to_dashboard_drilldown.ts | 279 +++++++++++------- .../reporting/hugedata/data.json.gz | Bin 33744 -> 33744 bytes .../spaces/copy_saved_objects/data.json | 4 +- .../kibana/dashboard/sample_dashboard.json | 8 +- .../kibana/dashboard/sample_dashboard2.json | 8 +- .../kibana/dashboard/sample_dashboard.json | 8 +- 58 files changed, 1122 insertions(+), 301 deletions(-) delete mode 100644 docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.test.ts create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.ts rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.test.ts (82%) rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.ts (91%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.test.ts (52%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.ts (55%) create mode 100644 src/plugins/embeddable/common/index.ts rename src/plugins/embeddable/{public/lib/embeddables => common/lib}/saved_object_embeddable.ts (96%) create mode 100644 src/plugins/embeddable/common/mocks.ts create mode 100644 src/plugins/embeddable/server/mocks.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/plugin.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/common/index.ts diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md deleted file mode 100644 index 9cd77ca6e3a368..00000000000000 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) - -## EmbeddableSetup.getAttributeService property - -Signature: - -```typescript -getAttributeService: any; -``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index bd024095e80bed..5109a75ad57f06 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -7,14 +7,13 @@ Signature: ```typescript -export interface EmbeddableSetup +export interface EmbeddableSetup extends PersistableStateService ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts new file mode 100644 index 00000000000000..fabc89f8c8233a --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { + ExtractDeps, + extractPanelsReferences, + InjectDeps, + injectPanelsReferences, +} from './embeddable_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { SavedDashboardPanel } from '../types'; +import { EmbeddableStateWithType } from '../../../embeddable/common'; + +const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService, +}; + +test('inject/extract panel references', () => { + embeddablePersistableStateService.extract.mockImplementationOnce((state) => { + const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record; + return { + state: restOfState as EmbeddableStateWithType, + references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], + }; + }); + + embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'refName'); + return { + ...state, + HARDCODED_ID: ref!.id, + }; + }); + + const savedDashboardPanel: SavedDashboardPanel = { + type: 'search', + embeddableConfig: { + HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', + }, + id: 'savedObjectId', + panelIndex: '123', + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + version: '7.0.0', + }; + + const [{ panel: extractedPanel, references }] = extractPanelsReferences( + [savedDashboardPanel], + deps + ); + expect(extractedPanel.embeddableConfig).toEqual({}); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "IMPORTANT_HARDCODED_ID", + "name": "refName", + "type": "type", + }, + ] + `); + + const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); + + expect(injectedPanel).toEqual(savedDashboardPanel); +}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts new file mode 100644 index 00000000000000..dd686203fa3516 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.ts @@ -0,0 +1,82 @@ +/* + * 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 { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, +} from './embeddable_saved_object_converters'; +import { SavedDashboardPanel } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddablePersistableStateService } from '../../../embeddable/common/types'; + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function injectPanelsReferences( + panels: SavedDashboardPanel[], + references: SavedObjectReference[], + deps: InjectDeps +): SavedDashboardPanel[] { + const result: SavedDashboardPanel[] = []; + for (const panel of panels) { + const embeddableState = convertSavedDashboardPanelToPanelState(panel); + embeddableState.explicitInput = omit( + deps.embeddablePersistableStateService.inject( + { ...embeddableState.explicitInput, type: panel.type }, + references + ), + 'type' + ); + result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); + } + return result; +} + +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function extractPanelsReferences( + panels: SavedDashboardPanel[], + deps: ExtractDeps +): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { + const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; + + for (const panel of panels) { + const embeddable = convertSavedDashboardPanelToPanelState(panel); + const { + state: embeddableInputWithExtractedReferences, + references, + } = deps.embeddablePersistableStateService.extract({ + ...embeddable.explicitInput, + type: embeddable.type, + }); + embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); + + const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); + result.push({ + panel: newPanel, + references, + }); + } + + return result; +} diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts similarity index 82% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts index 926d5f405b384a..bf044a1fa77d10 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts @@ -21,9 +21,8 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, } from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from '../../../../embeddable/public'; +import { SavedDashboardPanel, DashboardPanelState } from '../types'; +import { EmbeddableInput } from '../../../embeddable/common/types'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); + +test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => { + const dashboardPanel: DashboardPanelState = { + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + explicitInput: { + id: '123', + title: 'title', + } as EmbeddableInput, + type: 'search', + }; + + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); + expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false); + expect(converted.title).toBe('title'); +}); diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts similarity index 91% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index b19ef31ccb9ac0..b71b4f067ae33d 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -17,9 +17,8 @@ * under the License. */ import { omit } from 'lodash'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; +import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { SavedObjectEmbeddableInput } from '../../../embeddable/common/'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel( type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts similarity index 52% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/saved_dashboard_references.test.ts index 48f15e84c93072..3632c4cca9e937 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -17,8 +17,18 @@ * under the License. */ -import { extractReferences, injectReferences } from './saved_dashboard_references'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { + extractReferences, + injectReferences, + InjectDeps, + ExtractDeps, +} from './saved_dashboard_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; + +const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService: embeddablePersistableStateServiceMock, +}; describe('extractReferences', () => { test('extracts references from panelsJSON', () => { @@ -41,28 +51,28 @@ describe('extractReferences', () => { }, references: [], }; - const updatedDoc = extractReferences(doc); + const updatedDoc = extractReferences(doc, deps); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", - }, - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); test('fails when "type" attribute is missing from a panel', () => { @@ -79,7 +89,7 @@ Object { }, references: [], }; - expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( `"\\"type\\" attribute is missing from panel \\"0\\""` ); }); @@ -98,21 +108,21 @@ Object { }, references: [], }; - expect(extractReferences(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", - }, - "references": Array [], -} -`); + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); }); }); describe('injectReferences', () => { - test('injects references into context', () => { - const context = { + test('returns injected attributes', () => { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -125,7 +135,7 @@ describe('injectReferences', () => { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -138,49 +148,49 @@ describe('injectReferences', () => { id: '2', }, ]; - injectReferences(context, references); + const newAttributes = injectReferences({ attributes, references }, deps); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", - "title": "test", -} -`); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "title": "test", + } + `); }); test('skips when panelsJSON is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "title": "test", + } + `); }); test('skips when panelsJSON is not an array', () => { - const context = { + const attributes = { id: '1', panelsJSON: '{}', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "{}", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "{}", + "title": "test", + } + `); }); test('skips a panel when panelRefName is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -192,7 +202,7 @@ Object { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -200,18 +210,18 @@ Object { id: '1', }, ]; - injectReferences(context, references); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", - "title": "test", -} -`); + const newAttributes = injectReferences({ attributes, references }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "title": "test", + } + `); }); test(`fails when it can't find the reference in the array`, () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -220,9 +230,9 @@ Object { title: 'Title 1', }, ]), - } as SavedObjectDashboard; - expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( - `"Could not find reference \\"panel_0\\""` - ); + }; + expect(() => + injectReferences({ attributes, references: [] }, deps) + ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); }); }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts similarity index 55% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts rename to src/plugins/dashboard/common/saved_dashboard_references.ts index 3df9e64887725a..0726d301b34ac4 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -17,18 +17,47 @@ * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; +import { + extractPanelsReferences, + injectPanelsReferences, +} from './embeddable/embeddable_references'; +import { SavedDashboardPanel730ToLatest } from './types'; +import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; -export function extractReferences({ - attributes, - references = [], -}: { +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; -}) { +} + +export function extractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } const panelReferences: SavedObjectReference[] = []; - const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + let panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + const extractedReferencesResult = extractPanelsReferences( + (panels as unknown) as SavedDashboardPanel730ToLatest[], + deps + ); + + panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< + Record + >; + extractedReferencesResult.forEach((res) => { + panelReferences.push(...res.references); + }); + + // TODO: This extraction should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel, i) => { if (!panel.type) { throw new Error(`"type" attribute is missing from panel "${i}"`); @@ -46,6 +75,7 @@ export function extractReferences({ delete panel.type; delete panel.id; }); + return { references: [...references, ...panelReferences], attributes: { @@ -55,21 +85,28 @@ export function extractReferences({ }; } +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + export function injectReferences( - savedObject: SavedObjectDashboard, - references: SavedObjectReference[] -) { + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof savedObject.panelsJSON !== 'string') { - return; + if (typeof attributes.panelsJSON !== 'string') { + return attributes; } - const panels = JSON.parse(savedObject.panelsJSON); + let panels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return; + return attributes; } + + // TODO: This injection should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel) => { if (!panel.panelRefName) { return; @@ -84,5 +121,11 @@ export function injectReferences( panel.type = reference.type; delete panel.panelRefName; }); - savedObject.panelsJSON = JSON.stringify(panels); + + panels = injectPanelsReferences(panels, references, deps); + + return { + ...attributes, + panelsJSON: JSON.stringify(panels), + }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 7cc82a91739767..ae214764052dca 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -26,6 +28,21 @@ import { RawSavedDashboardPanel730ToLatest, } from './bwc/types'; +import { GridData } from './embeddable/types'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; +} + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + export type SavedDashboardPanel640To720 = Pick< RawSavedDashboardPanel640To720, Exclude diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index feae110c271fc6..c99e4e4e069878 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; -import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; import { IKbnUrlStateStorage, @@ -97,6 +96,7 @@ import { subscribeWithScope, } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; +import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 38479b1384477c..6ef109ff60e421 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; -import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { DashboardAppState, @@ -48,6 +47,7 @@ import { } from '../../../kibana_utils/public'; import { SavedObjectDashboard } from '../saved_dashboards'; import { DashboardContainer } from './embeddable'; +import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 66cdd22ed6bd43..efeb68c8a885ab 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,14 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; -import { GridData } from '../../../common'; -import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; -} +export * from '../../../common/types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53b892475704f9..24bf736cfa2740 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -450,6 +450,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, + embeddableStart: plugins.embeddable, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/src/plugins/dashboard/public/saved_dashboards/index.ts index 9b7745bd884f71..9adaf0dc3ba15f 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/src/plugins/dashboard/public/saved_dashboards/index.ts @@ -16,6 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -export * from './saved_dashboard_references'; +export * from '../../common/saved_dashboard_references'; export * from './saved_dashboard'; export * from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index bfc52ec33c35c7..e3bfe346fbc07b 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -17,10 +17,12 @@ * under the License. */ import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; -import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableStart } from '../../../embeddable/public'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types'; +import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart + savedObjectStart: SavedObjectsStart, + embeddableStart: EmbeddableStart ): new (id: string) => SavedObjectDashboard { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -77,8 +80,19 @@ export function createSavedDashboardClass( type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchSource, - extractReferences, - injectReferences, + extractReferences: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), + injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => { + const newAttributes = injectReferences( + { attributes: so._serialize().attributes, references }, + { + embeddablePersistableStateService: embeddableStart, + } + ); + Object.assign(so, newAttributes); + }, // if this is null/undefined then the SavedObject will be assigned the defaults id, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 750fec4d4d1f91..7193a77fd0ec9c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -20,16 +20,22 @@ import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; +import { EmbeddableStart } from '../../../embeddable/public'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; + embeddableStart: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects); +export function createSavedDashboardLoader({ + savedObjects, + savedObjectsClient, + embeddableStart, +}: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 1af739c34b76a4..8f6fe7fce5cfe5 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,9 +19,12 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { SavedDashboardPanel730ToLatest } from '../common'; + import { ViewMode } from './embeddable_plugin'; +import { SavedDashboardPanel } from '../common/types'; +export { SavedDashboardPanel }; + export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; @@ -71,11 +74,6 @@ export interface Field { export type NavAction = (anchorElement?: any) => void; -/** - * This should always represent the latest dashboard panel shape, after all possible migrations. - */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index ba7bdeeda01335..6a4c297f258819 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -25,22 +25,34 @@ import { Logger, } from '../../../core/server'; -import { dashboardSavedObjectType } from './saved_objects'; +import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; +import { EmbeddableSetup } from '../../embeddable/server'; -export class DashboardPlugin implements Plugin { +interface SetupDeps { + embeddable: EmbeddableSetup; +} + +export class DashboardPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); - core.savedObjects.registerType(dashboardSavedObjectType); + core.savedObjects.registerType( + createDashboardSavedObjectType({ + migrationDeps: { + embeddable: plugins.embeddable, + }, + }) + ); core.capabilities.registerProvider(capabilitiesProvider); return {}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index a85f67f5ba56ad..7d3e48ce1ae8b0 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -18,9 +18,16 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { + createDashboardSavedObjectTypeMigrations, + DashboardSavedObjectTypeMigrationsDeps, +} from './dashboard_migrations'; -export const dashboardSavedObjectType: SavedObjectsType = { +export const createDashboardSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: DashboardSavedObjectTypeMigrationsDeps; +}): SavedObjectsType => ({ name: 'dashboard', hidden: false, namespaceType: 'single', @@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: dashboardSavedObjectTypeMigrations, -}; + migrations: createDashboardSavedObjectTypeMigrations(migrationDeps), +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 22ed18f75c652a..50f12d21d4db93 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -19,7 +19,14 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { DashboardDoc730ToLatest } from '../../common'; + +const embeddableSetupMock = createEmbeddableSetupMock(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: embeddableSetupMock, +}); const contextMock = savedObjectsServiceMock.createMigrationContext(); @@ -448,4 +455,50 @@ Object { `); }); }); + + describe('7.11.0 - embeddable persistable state extraction', () => { + const migration = migrations['7.11.0']; + const doc: DashboardDoc730ToLatest = { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":""},"filter":[{"query":{"match_phrase":{"machine.os.keyword":"osx"}},"$state":{"store":"appState"},"meta":{"type":"phrase","key":"machine.os.keyword","params":{"query":"osx"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, + optionsJSON: '{"useMargins":true,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"7.9.3","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"82fa0882-9f9e-476a-bbb9-03555e5ced91"},"panelIndex":"82fa0882-9f9e-476a-bbb9-03555e5ced91","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[]}}},"panelRefName":"panel_0"}]', + timeRestore: false, + title: 'Dashboard A', + version: 1, + }, + id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6', + references: [ + { + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + }, + { id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' }, + ], + type: 'dashboard', + }; + + test('should migrate 7.3.0 doc without embeddable state to extract', () => { + const newDoc = migration(doc, contextMock); + expect(newDoc).toEqual(doc); + }); + + test('should migrate 7.3.0 doc and extract embeddable state', () => { + embeddableSetupMock.extract.mockImplementationOnce((state) => ({ + state: { ...state, __extracted: true }, + references: [{ id: '__new', name: '__newRefName', type: '__newType' }], + })); + + const newDoc = migration(doc, contextMock); + expect(newDoc).not.toEqual(doc); + expect(newDoc.references).toHaveLength(doc.references.length + 1); + expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + }); + }); }); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ac91c5a92048a6..177440c5ea5d11 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -18,11 +18,12 @@ */ import { get, flow } from 'lodash'; - -import { SavedObjectMigrationFn } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720 } from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; +import { EmbeddableSetup } from '../../../embeddable/server'; +import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To return doc as DashboardDoc700To720; }; -export const dashboardSavedObjectTypeMigrations = { +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: (doc.attributes as unknown) as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +) => ({ /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already @@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = { '7.0.0': flow(migrations700), '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), -}; + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), +}); diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index ca97b9d2a6b70f..ea4808de968480 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { dashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index a58df547fa5224..37a8881ab520b7 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -18,12 +18,16 @@ */ import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; const mockContext = savedObjectsServiceMock.createMigrationContext(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: createEmbeddableSetupMock(), +}); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { diff --git a/src/plugins/embeddable/common/index.ts b/src/plugins/embeddable/common/index.ts new file mode 100644 index 00000000000000..a4cbfb11b36f85 --- /dev/null +++ b/src/plugins/embeddable/common/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 * from './types'; +export * from './lib'; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts index e180ca9489df05..1ac6834365cd13 100644 --- a/src/plugins/embeddable/common/lib/index.ts +++ b/src/plugins/embeddable/common/lib/index.ts @@ -22,3 +22,4 @@ export * from './inject'; export * from './migrate'; export * from './migrate_base_input'; export * from './telemetry'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts similarity index 96% rename from src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts rename to src/plugins/embeddable/common/lib/saved_object_embeddable.ts index 5f093c55e94e4f..f2dc9ed1ae3956 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { EmbeddableInput } from '../types'; export interface SavedObjectEmbeddableInput extends EmbeddableInput { savedObjectId: string; diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts new file mode 100644 index 00000000000000..a9ac144d1f2762 --- /dev/null +++ b/src/plugins/embeddable/common/mocks.ts @@ -0,0 +1,31 @@ +/* + * 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 { EmbeddablePersistableStateService } from './types'; + +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< + EmbeddablePersistableStateService +> => { + return { + inject: jest.fn((state, references) => state), + extract: jest.fn((state) => ({ state, references: [] })), + migrate: jest.fn((state, version) => state), + telemetry: jest.fn((state, collector) => ({})), + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 7e024eda9b7937..8965446cc85fa7 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { Query, TimeRange } from '../../data/common/query'; import { Filter } from '../../data/common/es_query/filters'; @@ -74,8 +74,21 @@ export type EmbeddableInput = { searchSessionId?: string; }; +export interface PanelState { + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: Partial & { id: string }; +} + export type EmbeddableStateWithType = EmbeddableInput & { type: string }; +export type EmbeddablePersistableStateService = PersistableStateService; + export interface CommonEmbeddableStartContract { getEmbeddableFactory: (embeddableFactoryId: string) => any; getEnhancement: (enhancementId: string) => any; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4dede8bf5d752f..a5c5133dbc7028 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -31,7 +31,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; +import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index db219fa8b73142..270caec2f3f843 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -24,17 +24,9 @@ import { ErrorEmbeddable, IEmbeddable, } from '../embeddables'; +import { PanelState } from '../../../common/types'; -export interface PanelState { - // The type of embeddable in this panel. Will be used to find the factory in which to - // load the embeddable. - type: string; - - // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input - // will be derived from the container's input. **Any state in here will override any state derived from - // the container.** - explicitInput: Partial & { id: string }; -} +export { PanelState }; export interface ContainerOutput extends EmbeddableOutput { embeddableLoaded: { [key: string]: boolean }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc9..2f6de1be60c9c4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; -export * from './saved_object_embeddable'; +export * from '../../../common/lib/saved_object_embeddable'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/server/mocks.ts b/src/plugins/embeddable/server/mocks.ts new file mode 100644 index 00000000000000..28bb9542ab7cb0 --- /dev/null +++ b/src/plugins/embeddable/server/mocks.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 { createEmbeddablePersistableStateServiceMock } from '../common/mocks'; +import { EmbeddableSetup, EmbeddableStart } from './plugin'; + +export const createEmbeddableSetupMock = (): jest.Mocked => ({ + ...createEmbeddablePersistableStateServiceMock(), + registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), +}); + +export const createEmbeddableStartMock = (): jest.Mocked => + createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 6e9186e2864910..d99675f950ad0b 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -32,23 +32,32 @@ import { getMigrateFunction, getTelemetryFunction, } from '../common/lib'; -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; -export interface EmbeddableSetup { - getAttributeService: any; +export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } -export class EmbeddableServerPlugin implements Plugin { +export type EmbeddableStart = PersistableStateService; + +export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map(); public setup(core: CoreSetup) { + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; return { registerEmbeddableFactory: this.registerEmbeddableFactory, registerEnhancement: this.registerEnhancement, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 87f7d76cffaa85..d3921ab11457c8 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition

{ // (undocumented) registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; // (undocumented) diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts new file mode 100644 index 00000000000000..922ec36619a4b9 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS + * STORED IN SAVED OBJECTS. + * + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ +export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts new file mode 100644 index 00000000000000..dd890b2463226b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +import { SerializedEvent } from '../../../../ui_actions_enhanced/common'; + +const drilldownId = 'test_id'; +const extract = createExtract({ drilldownId }); +const inject = createInject({ drilldownId }); + +const state: SerializedEvent = { + eventId: 'event_id', + triggers: [], + action: { + factoryId: drilldownId, + name: 'name', + config: { + dashboardId: 'dashboardId_1', + }, + }, +}; + +test('should extract and injected dashboard reference', () => { + const { state: extractedState, references } = extract(state); + expect(extractedState).not.toEqual(state); + expect(extractedState.action.config.dashboardId).toBeUndefined(); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "dashboardId_1", + "name": "drilldown:test_id:event_id:dashboardId", + "type": "dashboard", + }, + ] + `); + + let injectedState = inject(extractedState, references); + expect(injectedState).toEqual(state); + + references[0].id = 'dashboardId_2'; + + injectedState = inject(extractedState, references); + expect(injectedState).not.toEqual(extractedState); + expect(injectedState.action.config.dashboardId).toBe('dashboardId_2'); +}); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts new file mode 100644 index 00000000000000..bd972723c649b2 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common'; +import { DrilldownConfig } from './types'; + +type DashboardDrilldownPersistableState = PersistableStateService; + +const generateRefName = (state: SerializedEvent, id: string) => + `drilldown:${id}:${state.eventId}:dashboardId`; + +const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => { + return { + ...state, + action: { + ...state.action, + config: { + ...state.action.config, + dashboardId, + }, + }, + }; +}; + +export const createInject = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['inject'] => { + return (state: SerializedEvent, references: SavedObjectReference[]) => { + const action = state.action as SerializedAction; + const refName = generateRefName(state, drilldownId); + const ref = references.find((r) => r.name === refName); + if (!ref) return state; + if (ref.id && ref.id === action.config.dashboardId) return state; + return injectDashboardId(state, ref.id); + }; +}; + +export const createExtract = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['extract'] => { + return (state: SerializedEvent) => { + const action = state.action as SerializedAction; + const references: SavedObjectReference[] = action.config.dashboardId + ? [ + { + name: generateRefName(state, drilldownId), + type: 'dashboard', + id: action.config.dashboardId, + }, + ] + : []; + + const { dashboardId, ...restOfConfig } = action.config; + + return { + state: { + ...state, + action: ({ + ...state.action, + config: restOfConfig, + } as unknown) as SerializedAction, + }, + references, + }; + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts new file mode 100644 index 00000000000000..f6a757ad7a1801 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { DrilldownConfig } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts new file mode 100644 index 00000000000000..3be2a9739837ed --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DrilldownConfig = { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts new file mode 100644 index 00000000000000..76c9abbd4bfbe3 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/common/index.ts b/x-pack/plugins/dashboard_enhanced/common/index.ts new file mode 100644 index 00000000000000..8cc3e129065311 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f79a69c9f4aba5..b24c0b6983f407 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -1,7 +1,7 @@ { "id": "dashboardEnhanced", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index b098d66619814e..451254efd96485 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { - TriggerId, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { - UiActionsEnhancedDrilldownDefinition as Drilldown, - UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, AdvancedUiActionsStart, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, + UiActionsEnhancedDrilldownDefinition as Drilldown, } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { - StartServicesGetter, CollectConfigProps, + StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index 330a501a78d391..7f5137812ee324 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -6,12 +6,8 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownConfig } from '../../../../common'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type Config = { - dashboardId?: string; - useCurrentFilters: boolean; - useCurrentDateRange: boolean; -}; +export type Config = DrilldownConfig; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index f6de2ba931c58d..5bfb175ea0d00c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -65,6 +65,12 @@ test('getHref is defined', () => { expect(drilldown.getHref).toBeDefined(); }); +test('inject/extract are defined', () => { + const drilldown = new EmbeddableToDashboardDrilldown({} as any); + expect(drilldown.extract).toBeDefined(); + expect(drilldown.inject).toBeDefined(); +}); + describe('.execute() & getHref', () => { /** * A convenience test setup helper diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 25bc93ad38b368..921c2aed00624a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -22,6 +22,7 @@ import { } from '../abstract_dashboard_drilldown'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { createExtract, createInject } from '../../../../common'; type Trigger = typeof APPLY_FILTER_TRIGGER; type Context = TriggerContextMapping[Trigger]; @@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + plugins.uiActionsEnhanced.registerActionFactory({ + id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, + inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts new file mode 100644 index 00000000000000..9f4141dbcae7df --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index b3664362009149..ade78c31211ab2 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -7,11 +7,11 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsPublicPlugin + uiActionsEnhanced: AdvancedUiActionsServerPlugin ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts index 5419c4135796df..e1363be35e2e9a 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; export function plugin() { - return new AdvancedUiActionsPublicPlugin(); + return new AdvancedUiActionsServerPlugin(); } -export { AdvancedUiActionsPublicPlugin as Plugin }; +export { AdvancedUiActionsServerPlugin as Plugin }; export { SetupContract as AdvancedUiActionsSetup, StartContract as AdvancedUiActionsStart, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index d6d18848be4de4..718304018730de 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -16,7 +16,7 @@ import { } from './types'; export interface SetupContract { - registerActionFactory: any; + registerActionFactory: (definition: ActionFactoryDefinition) => void; } export type StartContract = void; @@ -25,7 +25,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. } -export class AdvancedUiActionsPublicPlugin +export class AdvancedUiActionsServerPlugin implements Plugin { protected readonly actionFactories: ActionFactoryRegistry = new Map(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 43b88915b69d90..9326f7e240e3e4 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); - const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'dashboard', + 'common', + 'header', + 'timePicker', + 'settings', + 'copySavedObjectsToSpace', + ]); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const spaces = getService('spaces'); describe('Dashboard to dashboard drilldown', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + describe('Create & use drilldowns', () => { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + after(async () => { + await security.testUser.restoreDefaults(); }); - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be( + await PageObjects.dashboard.getDashboardIdFromCurrentUrl() + ); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); }); - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + describe('Copy to space', () => { + const destinationSpaceId = 'custom_space'; + before(async () => { + await spaces.create({ + id: destinationSpaceId, + name: 'custom_space', + disabledFeatures: [], + }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - await navigateWithinDashboard(async () => { - await browser.goBack(); + after(async () => { + await spaces.delete(destinationSpaceId); }); - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - originalTimeRangeDurationHours - ); + it('Dashboards linked by a drilldown are both copied to a space', async () => { + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.copySavedObjectsToSpace.setupForm({ + destinationSpaceId, + }); + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + + // Actually use copied dashboards in a new space: + + await PageObjects.common.navigateToApp('dashboard', { + basePath: `/s/${destinationSpaceId}`, + }); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + await pieChart.expectPieSliceCount(10); + }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index c524379640df7e2c11cd0f86603cf4048a826bd3..c616730ff35b64074b036f0bf30b77d0e45b6c75 100644 GIT binary patch delta 32038 zcmV+0KqSA=hXT-t0tg?A2neVLq+hWJ;syjynVid$?grC;Q+oH~2R~L1CmBBe#)bX! z4`?(qfg(N|L~D1HA_yDWZD3(!|u)Dt}j}X2pU)sLQVm)&|(=hLv(1fa|r0I57sQTOEN^*3a(!Vfa_NUE+kil zDRAT%bI6F#a`KzN_43detCt4_DCdJmpR%W4MsJpOm|Eb3b#TJ%aIz7>fVC!AXMM8% zS@wrp`1T4wm3ej*s7OF1Ym*7dLdr|R04M;piC=$zNpDd3D_ClLA0xNX?XLz}r6#DO zHINnVk3a8^hnwE~{YeZ3G-B^AN22j5`k3PqGcYYIl0kd@x-(p_Uu(9Qy(i=HnaF9d z%eYW7#p*D*IxxBIU?S@aCS%ABz!A$@Qc;Opxh?eR$KA2BLXiY2R5`koTqYixC^B5y zUXxsZ1MAf7?zXd1kpv1D5?RF-@Y!4!zXM`G{G z2q#KNRj2My38_+&r6FZQNaWMo zcsGE8kr0|aj#2Vr=XJb!=}PCOuF6X{oJnV^yKqt!jvj8t#tKoWd^9ZvrE zrn3be-)Qb?oT^s^pvs)P4%9e){rGtI{?NOaItwwrW-(x=?xp9GT6!L)nV|+yWj3e- zMKm;Jil0fTCTjXDI=V^S`E*kulU$aNVG$IpVsK~^g?>I#R2Y`j9h!8wLoKk%axyDm z`DlWVHjtb0nNfQlVb~-qQw6PmGIF(o)*$R^Qmr?Kb8pom5mc~ZEW2s98E#-qSP48Taf7tmuV$PAlGC1*}5WumTpim{LaLymy3HAKvg}h3DSw+p`cF zBhA?b1EgY@!3!^<6h3d#-f-W0;Okz+DFu@ub%%;&(9jxV z?9x?(tBk;}!Zl7u^8MSppSmm8-A`)5&jyz)`FOC#C!qw^PPHmvRjOGkR%675q*p)o z)u|u927M~Ws7RMIjH2}^AVthRMQ2kI3KG&(Ty;%ilC>Od0)*!jiNweN{SFCp$R2kDWJ?Op9n6uER-D+~M3RH#e zN{1?VM53WSd7D!ZqMKG_`N5&W5Cd8=!^~_{h;5um7q;Q+6_6^V>l#v?9zlv~f;B!E zyR<$h&ZBM<2EN;W_wLo6Wze!PBeZ)=e7(lzxkAFOZJwiGIay}vm&}_3525v*y zyRYe60n0mx(O`Dmc4e3OPn(-~_KRY@?cDJ(%W&NaEr-m}hZK1CoRE$QO@cwJhE^dL z)zQM339OvCK|6*dOgxK2yjv9JE5TK4i8@?F9~>mhLlY2xP#_OYxWf$B`L1*O<1B*~ z7Y@@adb=y%E?S6{vRV|4BQA>01k47k;I(X%iBwkys6tAr0c9=ym(YqHLrOwKXeE}l z4JCc-UzvDRlI@0%jzSKkPG|rH-wv%BK5lovc8BU$Jtq^ri<$XoLJ;QXo6N&8Y1r4; zdsW66_UhVyE1-wZa?cfH!zII#aC@6D<=c2;jIcSP+!tF$QE+Wwjb`xsa09Fog`3A5Rk0U zA<=d4!mKvx5!qI9u>5k{X~;=FpKw=a94TGM1Ei9yMF=f_g2@nx5t!heu^yL*iG5aEP^emf6_Kb_ zz)VQQ3Ve*1Fb83ZL|nbFMZ`yfB}593WAHh}Amr4=2XKo_LKUD&wMqfX zWu6V3_x~p=%#cKE)d*7ssxscW0#(c*K_)Lx!LiWO5pt7i>Bamyr(7lxu7braRh-Cw z%S0Hm@ku%JKgdoM2hAjbGZmaO9CjJKp>(IuZk6m}Vg zCROK)L5^ULUxPW$X^75OURZ+bP$h%T8$MuSBKRz>p5lKx|$q% zdIT{%v*}}t?wC4Y6}u}HtU-E^FJ@SQix%6VdU0@&1u2syRw?iln&L`9j8!EW$9wvs zi$a?Ww%V;-39dq#wNeht)}Xh47K5kAP}-8xE^Ar^twPteh86`p$pql82kspeqMc@8 zbob(+ats^Ld*{es(Q9bQ<1h0K;tH&7tgw6WPyvcBIwM~UPvAnb;h7q>jRAHq9cmh7 zJy_%&MzLrTBJk&L(W^qJ%>{Ug-w+l-jFN>G)5g@h{?aR%A! z%WBnIslI}XjIIhyWeTP81NO}^8S?2AIHzaQsZF{@-|vp){=5t+7D3A}U`CHQMu>ql zY=M=OH{K)}Qvs;55KxC{h(wI7gHxG@QgIsN^bO&(KlQ#6#1d#4<++`~a!D|G6lRo~ zto)yldbhvbcTTTggwXJR8sbC(PI%RTt0iWtKrf~H!-9NZ;Id!O5#M9{z*V}Tl6!0Ns1<06C>HuDPBK(8WLn~jiX z7s>k;Rh(LYmFt%Z*dTkO4Fh&8N2`d#vjiGBb)HNya50C^C_M9jJn9x>IxOf-jV97U z7BNflmGB4__S~J+&r%E@0>MYbIu{E+h;bD&hlnXSkBOK?b^@b_uGN+2dZ1M>O${`g zDS+fWp!FevbmJ@7CVNwCP0*?srvh5aJSR#O*W4QRuhQoV4Dn;J*b zih0J3b|;OuAx7_iGQ9)Q6P*=q^@?q7TVxEYK~}D2DrA=UD@2_*2 z{U3nSjbf9LNSVP900R%{3xSeUGuz7hSAeTny%e}8ICALOd)XLJO5clv)LXH#(;&3$ zwCearxy)V>wF-K+g4!4nfO$ggFhW>9iJ@uQ7!8)6fHnYs$2jRPrg1hz!i>*n25`#~ zlB8!_r8DY4R^iRmA+wRx3#>K4I_s0r_$=w!cF*SF*m-L4gBV&;Hbf*s83E)yV&UAZ4ZbwcOX?Wu>p>evOum(Zh4W?C@#;#7GDQuR*ru&Gxvzz3pz1B!dFi zINg~nYK4Hj)ZOpZ{KX(`1Q&BW_YaXshVhLNE^xi=-bgSBp#e6=vh*kY8bZM|4TEGT zgyf@8k@t5AbXA&H*eIH;f>&uQxq=rD3`OwH2TzWF?3u$=8wHY8@G5>Db-WNUpbaUQ z1QDd}i&k3)XIcB_U)R!z(eT!@KzxEUED7^_BBR)-Dn3L_{Hs_iYwO`v!6eneD-)CD z?XrPP(@QgIt;YFC*0&=Rk>2V>w=w#JXM?N}L6+L17C=HIx}GQ^a?v9FvwR@4T5~c8FV);wBLX#xx*8$gH;dTwUO2cg(t|8|9E{g%0QeRjLN^k4oRjyqsUY6oT z!YdP4;In{N!_LDI8Yxz~C@27ejNDJ10qrt>Vr;brv=Umy#;Bt;#+^=E4X$Ea)W>IH z2BN3dd*Ide%Vw}9`J0O=4v?I`?Nq89r$UWVa7vzl^4U{dNFk53C$80H zJQB0JOIP1N>moU-m>qfbC>&05P0ES35>ln6ry&I?CXbnjyY*4HTqd3r-X^JBqpn|= zo6)NJ5V;&?d10!LV1$zpnA+uLYJgRLs#gkFXe>GOfCN>}Mrhi#&?I)Ao%&t*tb3_j zztaZYU8eZPB@0~E$-)Uwsh55m$NpH}=~n32AGJQWLAEGBC8!Eh3@TKEtfa?EP!)Q1 z4JzP8&WU`wb-@{sE?Bo4X0HHMp6xZrl-|VQ{>u~ z4IV9`&{G7V389*ms(0p-B+&f%Fx9~ObLWeIEP)o!j4&F^K%L)q#_9a_8cb**5cR#1sQ&8zMgQaoi+R zS_!c7$590=aWNGTIj?nprM02u>o`I*be-GVWD!ix;Ia>7mCJ-gG)>w?p&DG}?n{9y z+L#QtBJ%;y9E7~7xLmio|C-_2_paKr2udWBIdcOs^Dn&-;%u>TW+P``0jyG2uYwiO zK;R|^n>ji7%yy9Kj^13ry50BZ^^+9iCvq;34dCkcitic*E~o~7SF!U_;TmMBX@={5 zcj$g|>LP`jC=4EXOo!u0ZdS*en(K{m$ZFIGgX zlvk`G<`a0x*=G;lMxiNCf^4H0vJPaWLFBrU4O3I=KP`P$=UKsHa*?PjUp8>rF8e6O zh(*#A0-dpEW^a#!fYxuF*9@r{i zm8zBo7LpwXbSWY_f@=X)y910>peiJ%x?Z`&MGkPmnk+?sKcZS~!%XkXTX9NFP;0~) zhiKL4oG;{AYUlh&iJJcvq`1`xU`o82L(0#>QNQaeyB z@R~7jXt;4C3T<4?u795^43L##rIisgW^}>xN(>vawC177==24u8vUomXsHdT#v(*A z!^mmn5K!2n)oj+BWbK|!CJ{8|K#m>2^C=K^^Fr=_yz;iPB=H^`myul|r2uG8%dojrGEkC~>R7R$i+DT*Yong^N`s8Aq>TiY^M-88JNE z%5PPFfUDSXX>gh7h?imrfFVT)g6C?&Zg;)M_E`jl$$wD-TY}}AnPL^N$}CX_ ziyVBU_%LP)r+t(TiPO2^6FTxl}YM)c$n4?;RS>LTIdi6jH*Fr=6TgF6()zhfREYCBVwO`zl}q z-MXD*?Oh6+g;3Z7kkfJ$_HY+7k*y_r5XU(;Y5ikaN5JbUTtM-3$j(Iq%LNpM#}LJf z1J&Vj)!-^yC9J~54nJ~A-e!!(^F@oL|B~XKO_mA&eCRz6MG|PPes%X!xk*T_^%-;)$ZYDe|sp;I~3{FMF@?`N4`EvG}WSaG5Mv#YE>26 z{pGwrb%u*YQ1i?5h;5eYGggn?4<=6{Xo@w)`ek-nkB^6A?=;Ot2o10? zW;bm;zzX$i4X{CWp>wFXt8iwfEKb~iX=i4*itne3blm^og;x=!^;?_F1@A@};o0d^ zDLA#^h8(yTJ_&V1C*}b+!>fGVcR5_zX7qt>UDScAtk|;#*C1tTbAjtGoukA_2JK-S zXWqOHT;;~7!8OLLxvhp)Szx`YT*GXYXGwn@z!ZPIiWcx@HxDUCV)rR7+vBl+U2?t- zTIH6gpoP4tEVyiNQZr$zn|Rk~vuCj!uJUJ5Q!ZXm=765WKuRXG1wxzqew=CEJ#y~`=uJ>LClHfX2-XKw`WIbt^4}xQFH-TRRsnR!4LCQG~fYI^t_#gy-H?aw?!b(7u z`YH{m)d#T2?&aB2b)WrZ5w2r}JjiM@`F78y7?*10nw_6{^{#ivA4zZxD&YPizU^*p z&RYJmz=hI|wdW%tOJXM}uJY?H9Em7prM!0S*M(xyx#!9|GCOR2pe zRtT#bpsNH`S#nQ-YHFN+3BeGr%H-djMPcMu;_I8F_ikR_^=>noWzYh_O#Cx}&p`3B zu*kK^(px50@46GKcN$+9sK01`+dE>u)i*{CpixW}`rorqZ&@HvKx305&X7zr!d|f^ zW0+V8sWPUZBgH33V@M&v^a?IZf#Rv@O_)Lrs0xiwgNigNL$nEh6R*CI9=~SQn%hI~ zY7I%Cq+fWSrjH?zUPUY2)D#ozn+#CYfT~cbRH(djHiQ_!^2IRW*t^*OxCu?Tt+8Xe zj3?acE2ak_3bP3wyB|cEWVi|!AHT8`G*f`X{k76eK)b~tb>J#iuIq43F|*Gx^CKwq zJti??-Xy(O2dhGV(x9qV_KZp#YZ`cQ!-(R!tzmCx_mb3y|r4Qo0 z3fCZI!CAMuk58}&T1Ys=uIb5bz3blI&LU`vHO4^?TMMj0Gt|NIF#|hz?}11cEL&=& zj;n2=op+DrSQRoY5v?@w+c>Q9IVK=Ea)A1WuDaBCG^ep<7p>0tl85dO$WDLn3G|;24`_CSUb)_e|fI1%b_YU$$@e!{lHX6<~vOZ2+4 zB9;!;nqWz%lAu=2W>3puD(g+s5t# zV&&#osA01dDq_Y*zH7y6G_!EULqe>&j$IHtygK&>jD)z37oRpx(Si4viJPLZ39E@w zegtfP02N~?G!-MgBDi9N7|5$VD@5Gpg{rd7ok_i_^tu-+*GjLu4KZ5) zw3zLb0(=%++bGwY?lWI3g2pE(ns$)_>KLqzLeFiR^^mR>ScR&!0@fI7%O}k`yg7E~ z^(=!DRXcRa@mVVe&RW3^+g!AV39jzVGe@m|d4?A{MVo_-K6)=q381)DW|Q3KF3R07+cSP~t(Z zN85zKVF=Q7Ip3swIAPVl*ZZjUdLw(P43Qw?ei32EyKNT|)&Q%}2z9UmamxTi7$|-Y zW|>eHcad&mgb(}jA-Y1vA`vuy$}q+iQCADBLd9AGYlzK}6IhQUq~0I(QpG5MO{Vzk zp}&)785FpZ@zzCNVQ%RkY2$4>rd0*5LiJjKYmlBA4tGxbV~wM$Wf9LTgqj2d%mIj+ zI$~c!>Y>T`AzKfxGILzTYnbIBr^Anj-jh(X4C9B?IK$O>7-|whHN_EsCJQF4Dy&N^ zgi76(I0jOdk?Kt#PU?H;tPL*Nl#$c%!kP#1V8s^fp$bw(+(Admx=cjN2MVOKvDOJe zPm>{n$^6PkR0^$68BZCd3})*9RcL$~P{S-P7#*k2os@8Ys2QcIP|hhNRBACN1|n0u zHp|C^E-cBXxU*mzI@P;>UcUlW&OCsR;0^LTurS%$#7kYEs!*dS21~7d4dv8pz*Xw6 zR^S?@s!F0BE8HwB^;$>u!lQRS5m3CKjrWcTS(#Q>D=NWNYK$sez{z*Rw`jeMJnFQ7 zOWd5lkulbQtJD}(xDe2Ko&XLha$$O@UA9$HSr=ViMmtHG0^7<}^^Xj-c<0MR`!e3~tuCU}0jtVQ?suQaf1%-o8FiGspRA>wjOz&9y*T^!b~cZb782rb(2 zka7szKr@vK7l1XHKY6mmW9MPHix3K4gj7rv@}M^nFbG6T^7C7fhZ=wt^9LQUp^`OK z04sN3I$%R=Jo<=N=PrjQDKsAjY?DO3ocGS>rG_y98Y&v909UawYH*Ema>iDJtC&mZ;{=vo zM#w}XoB;%3k4TeR8N+qAyLsq*cx{$JbM<44LQQbJ>W$Sa)oTs0_|;56z3rW=XA#u+ zXOug2Bp>47QX;!Zxv`bemfEf979mW)hKnVB6 z+U2?KZhB`0NdiT;$o|klc7Y-w%tG8F78y4(#Lhc1R-oc7)j%p`jSohsFuEp%{z<8N z6L^(>y^pbY+10_RP@%q#DPWSb`;g|Hdacel8zPa>gaHD$WdPlz$!)g1?)zzG8Lor% z<$cX1ac@FLaYE~t-q-zR85Fi)yyMk0#=40u2ulE^(Vk6`Gj-r9RW2Pa@Co@AFc>7| z5-!(?xvWhJJtXR5?@Z|=gTfl3C06BaoHj0h8{r^`YtnQ&dHK6?=3eOJ@3ffEWCzxH zJ}nk_u3#Aywpwk+g7z*?vj~chcOe6LEg;QuE(rZ@t!^l@pcNwMLLbjEX!K#2S$vZ8 zwt~|t)~#F3YYj3Ld~sUk`S)V4rKR6VQG7<^0gPZF3jCo7Ic2aOdav?f2^6SOc;Y$HQ&+KzbHJA@IR+<542NMWGKa`)F*lG1p3X724q%UO-U}nv{rhA@Nfo;E@tq z+t^|qzzWTA4KQ+E8H4jahnT#yRlnJPPLYQJvNaESwq}&_JsafXuKP|c7C{4RjHv=M zgH-{oGG{-@pb;)e2}7Q)sIW;s?{VCut*s7RWyzW~xI6`k8Tfz^dwXF7qsc*1tcF&x zDe4REh|EJm$cAaXFniu4Z+bDs-c$N4!szu2s0CHA73v$wh|yEf+$qY@>eH^|pO2r-{fxOM1Evwou*NEPao zhLj_CfU`5!8w`(FhhFf1Q{qmoZQj1#y)$ZkB6-5dXHPL8imQBDT}mM_d*@>w zHImL%FN-`^4%z!8K!weE-XHsS89nMoY-WWso7q#kqy>=2o0c(&G?pO<~!fcfe);NiOwfj8OzHgBSp>-0!VG#p%ufr>t2w!TXXM2 z%oIW;sEWM%3RHtQ_gSl|9KJ7AthK}UBk5M}^)r(Q8dwf(=H(#htq)eXP~PhBeI2k0 z9sCMdW02C>6wBAxmD-_yvcnE}4O9TfOH@J-ctevi7ghqQh>z+(5hp-rv&q>3N7Bpi zxz%#SDoB-@pSBh;8}7XdU}BCus+9TKJBe@&EbtRtDPwhIa8{7Kl*8J}2s_tLb+AnGJ^*m2!?OW@Qs;JR)zmjSSZstD zh=y87T%3qP_jZ%voSBN&h@VTjsD1@&kY!@F8eGM?rNiZ{OM#rd57`on5Ssd$%=gaf z)j!bDc1B{%JV_CJ2-yp>s^Uc978%kiNR_^Z3Q{)slyh(pv*8iUWr)+N$-N3vr530m zWgU_CNZ))8#z_NzC%(z1TSrjUm=-Up(%-4G;)5I+V5A;zIs^51a~-FQKKo3bDsaj@ zN(;oAtpeHgUre{t7t;mq3*|bF&k?c}*1n62Q#P4ks)ANo-D?G{LG}RK39R0m!*<%* zFvBNU>Gk)SAPWLnXtLMBRRXH4`AP?B47Wact8%wq=&KZew{D^#MwK&1Ho!BgOGnkL zNu~l&WxWh4Pyu5^@>HH;W^aXBm*~+q>13z?RG~lDfZ`TD&zq66C!nIRtf<*6Y6YMQ zo%vOu073+7LzwQI7-5qPq|~h8e|OWHrB5O#Q8KR!OM%`zQY?ySgkoTm1*5Y=?_A(w z2^0hT@`T}k^DfhFvZJU5RHYWE-m@B`Y&tvj-nEEHgs+6A-`wtd4|7d2T!)L8f&~}2 zFql&EUMSu)sYbYJaFzOZ6)uxmS0aIh*0FX835IqK{Pgj)}V< zh2p%#9X9guHP9-zMHQ_vCQL4H4S<#cRwCwrfv|Fa+4@gQrXM51hP<=_SjB!z1B;)+ zY(fYLY!cSUxMt(cyPNK7WLN|(gof;LWFjAZ%)%0xHbuc1tO1S$>{gBhjC2fUS7Gyg z>5Hf}Kn`*o4J$!aO5`=DQXq~MoF(N7$x63iB>%n8p)k>f9N_5{wRil$n9*-Wa2AuNY&yHq^qZu->d<1u0nw znf{xy^I4cwlE~*a1?L)A6^1lARuuhBTfvA|ao$SEhqkLtkfgW0nF7l&KL32uts2)R z%GFChH!;q(dzQ6(fWtCeM+*bFuWYRK#7C_YoR@fsa2qyS1+HTLpuq*n=D^cS+zmp1 zu73G0iVN?ziE`*0>91VcSs{~%MUZA;Mt>gG!Veq)sIU zSp#A#LRLY^zJg<47rxs4T<9c&reI@#EE2QT;3`+I@s-O}L#vppDrk{j0Uu=|c2ABA zrIv@#E+bE(b}t~Z48tqe<$3JhxO`OFaV-zoljDkLB2m_*&uVj96>C9N?BlOO1$^B& zM8rTef_|~Y3XOPNb(6~`ENS z?UR0upk4}GffOr;Y?ATbfYdYDrXLro!Bt$@(BR6E>(w49%=RWrwdyG4yEa)kT?ejW z4xzwB4BjEcawQUX_P8h6;sr!SOM~`QV0-vG!cbz2AU*QvA5!<0}zO-U?H*6OtDX(D9AY>-)v)> zZPZ@@R=lMxn(34%nIchoAv@oM$v-))0rEDF>I#T4ZZcpsxXPWDz5>D_1v~`XG?O9J z%9@-Xx7FY(H$@GuWFQ8Aa_|%=IxtCS;%(B|^<<2_PxehRd_BfEK$1&uA+e4)JOVwA z^sDr|LhMGi`0lQ|DZaZKKLIg=bsV69P~8M zd%#tLt26{QNh!oo`W~l;Kp_^ZUYozhAhwi5MptsKRrr`kDqIWR~y-Dr3X$7cC z^-2w>KPRTk3PzKEjm#5Lf2rZ&3$gl(>fs^AWQmamp@z}qA^~S>dZ$fQ+AzlG z$^=vIodF|T4ju34K3oK$+{SPKyvS4*>2h<=kNr7SMM^A(&}peq4O9)Q0aYP$(4iWGW0Ajlc=@ukb7u(@s8S#c48+|X1_0rZ zX0u+Jhu-U|A2gk^ynopSvO!CT3DJ#sn{^|cx_5ZbGH8s!S(_u;6nOqgXwnk5Rc{1W zf3Vc~=@Bk}9x_m!Z;3r98z>>8p{Fl};DP%|mv5hK#Xui!y5pp+>Kr7`?k1or%pDfx ztR@k@h6Eo8+IjmqOK=q_Fd;Ho_tATZ(qSwRM=G|M5qNn0y7Q6;mOyb;oMBJ&08-$` z;{r>lB%-iIL{kN+qKIY%DRw-`IN}N^a;<9tm3Xv&Xp1aN6{yO*`ifFzOa93bQwTA6 zAxv&_Ywcoxoon-x2-m^#2Fdg&>Z9n!2ceN6x4M*42dkn;c?Bz%a<<$p8bTtPzGR=V z$yoI4tvZi0u>{wjz?can2h6G{3>3zhtQaY8k$b)S5CD61gL>?ALiYm$m>goVhs8K3V!*tP-oZU-YC%wYu zeI$RLF;TR`%j=d|hIUF-0jpBATEPkunIBgrjHf{QSy*kOSI6!b2WA;GF6u3~K+z3< z{gb@(1h!hfRtc^`-O}MQF%xxn6zaG@K1xA8|uJSs$4o; zgT&Dn?dqPSArZb7zf}ROV%5^X8iGK7Oi*>FS_;py8J#Dcay$ZHg}rN`Np@z&srT&Q zB*Jy9JVs6`J1!cA;GEEh)hwlaf8L)uJ1Z7J2@fGg<9xuFrk!g-FIKb6Llv;fG;0Mc z>-c5`xpWE;t#QIos?-azi5Z?>z3q$@NuZ>YokyLVl*zDU>Q$75Vm6tTNc>t@nU7!?L8) zYQ|)6qX_Z;Hvc(MgEZiXJ(P?b3`(6emVorcOTWJ#+Uq~p> zDsjYQll__9k4S4kd9F#2dy2e&&o&9&eYwdf&NSUzVRF83x>?^cFvcBZtOQk|@u^Ua zvAlLxs{YG9lwBXryPREOALZ%w%fhzIPp3|{9-6NKR^imV29`+_EE8Gq6#W@1XcYu0 z0NiHN2MJnZuVJBUpQTW}))4C}p46+xb-8j4)33`7vOE(jLRRYR*B~2zq_FBPkoDfK zHpy@mE}O{VNAl|?x)g0*<{N@-LgO!ftoy?9Nd_hR&KDMJ!r(9jX{r^22AOYydVrNuuNA<5oDZIySN44B$_q2(P3Cs69$uyPxYo}$hDFW_R%6_-RKN6j z!;zM*Re`Hiy;k5FNxS+lAyv0(#%Op*+Qo#1Q_f4)C8lvUN=+OqL_npq>!?IP90LWD zA!M>iX7@q})Pi??C1^Q}&J$3G=s5GW1glk(NF}sN10WSG?|ck@&gJYq67P~4kXlWs zu&4t>*ZMgyXKirFri@^@0b&7Hlj`dQt=^UDS%&Lq0RZnnOg?5(GGY-+N8e<{-y8&$ z2iK($kP=%1r%oLrpRA=ITe#gW)?haZe|q0*(xCze7F;Bp08$h-m}8T+8mt6WDKFKa zB4=eIcn%e_6+&u%u?KLQ!hQv$N}bhJq|lHuAuo3b23=kfQj6jfS0;) zdL(14fL5_Fs%Q%4_b~n>!q=f%-FfMC zoUHXR#EF_3u`}1ZR>uTL%>EXrA6gyaoPetZRiXKXw@&Cr85)@VzN0& zizLBG58)*YuO@-oDAtPlOuA-GQSR~qz z=?&zSe@l0j#Ac&y5;RrdD%C9&u9)~uA#un+|^gxD?tUk9#2+MvM&;63jX;j@{} zL@Qms5?7UOL*T2xRrnsNaK)H>_7Uk~)MUv5F#Cb+JISOvGQ7huPThc1r)P<{ot*zX^d zGr%UA99$*<&;z1$fmF(LRd@VPa1HQG9u!)EkjNO51Iy)WBe*ariMrJ@d8oiDie3Ie zZ8aTeL)k3r051QaJ23APc*vRfKDqvXD6GzHa>aa(iz?ijE)9#+Thks~Ox*Y8ymwyO zO4%YPn$)YiW~s$RBC164Idn`Bgwi!M8%FG}QVLMuz=x2%C00)_q7?$|CQaWHGe3AW*-10?EKz~Yi3#LU0s{-h;X?GiUtGK1x4sl5=NH=hdBp`G8V0#~8m zT7fGhK7be_Cd}di#AfFl>Y!D~wbqo&22v~vcA_(YAULijjW!=_u{()a%Q_Q(pn!+d zH6{|77pk&N7RzHjz$%!d2G~e{;dC9qDp;fn*bvvCK3HV;s>VvfIdQ`-@<^voY?(`g z6vtS$X)1X$LZ$J|+6&1o61;%l=Lp#fNgMHv+@_Dd4q~Myxq{dj19&rDZ+Gvz!$mTv z!BFO@VV=C6wi;Z;%C!R5 zFvp@MQTxY@hZR;2WQPzeq(m%2pb(NZZWK0-P=;Np!e(AN!q+)-;OJ3GdbR6LtpivE zgVX>^&Rd&+96!+WD3PvzCYc=f-S2lVcb!KzSOPUAF~a^Wn2whn5mO(*lkaJO3 z4Xz?Gu?p81M~0u!>P)?68Lp#+Xfg0IGn)+0t1sIm#I+GyFt18~a22|H9j;LtmL|A5 zZy=_xq*!5$+`Y?MMQ%Q2fuXkQ?5Y7&;e)6E1;|sMkvHi&VqVhW0*P^L!cPykz3C}Q zpgn|R^mN%uP?cGq4%HZCMYg`%n_mu{J1b`yG|E6E)MJhjVlZau)FsBSi3Q#gJiTpj zl0k7;KF8!tplAnwLcl0EtX9`JE5TJpuvXw2q(h0(djIm!87q?DI#SLV?oFC{oGmWZ zs?8=rTM4K#=dJ>^K84X_w-o!U{>NQ+E5}Ml$!K9Mw8|`S1+8&n=Tn_*X&G9lwS4Iy zZS!^ltn-d3mOy<6M>*fag7#+bEP`r>BP`@H6IK3kz9G+fwehg19 zRSO)YnYji~g`QmlYLF)8$^2ecSV&oDelOLHlr94WHb5b?v*3j`ttK69c)_YWW6%f# z;RA*jmk~}Wpj$F`Z&I@;rw&|&&)^Eb8iX{sYH*e1vDV;<){|awJ8p{3rX=)wOD?{8 zD(iza?v9;*&lh|ULn9E8_rqH5k#_{3G+N!%CToCJ!7O#qoSzal#@n1c#^*(fo8%xS zVSn8BzA5ri4i&ZF(WmTrD1&I&0<~7dE0eVQ+X_{Ta&p09sIgFpMDG(ZR-ze8MQd`l z=E>|YQR*CiSD%G$xyoShs63$+%JX2rlz?4iq@AwSfCPV6C zJT9xSw$%c&DoB-@p@tMpEl7@OYjjbtL6n&OMo(jfrAftFr5Ipv4hSa;iQ-5j*dIW{ zN3r)Rt`)GnLCfoj1FwxuHVWmkh9P(FbzCc8ISR9Z8*SXQkba3(Y|^zgTVemi^GhY# z29qIwhKwe7ii0QwT6!$g#JgiHuu82^OHD++m}`>Fykj>y;oO$EdSn|be096)-8(tS zpi-<63P7$JT7`M96}0Fb^u}jT;SL&j=6T^QZhY@fH+Q|Uy1CP0!(kTnQxx0Vy(@fl zkk!Ms4o-!>O5^R3yg6W?SCI56`sHLWHt|${6Hv$gJ@Jzanx_g8bBHlH3;;n$(6_oP zQVFg?XQji%2myFR3Zt!aPFVNT>V_MOdg{CcVwOSCE{dLQj%ZWxCVL?%-)87yhO7Ig zpIHVqQ98ur<^`_)u1h=S<~+Hqgqa{qCJS?7v5DiFusZcVA2o@fC>F)eka_z9vr*}P zfPAxw;3`-Z`uG*B5`fFRRfL><@&b8p!bqPKYXr^G;2LAF=PN;Ly0a2eG%f=Di?r{w zsU?nJiYi>3)v{!fDOzDFA8eCBs41aUA?hj2XK7*2C@q4v5>%yDs6jPGr^sxA-Omfp zGJGWoni|JL3R5)t`H&38XNsf2IUitunSr-W7GppytP1O^E2ma41ZN#WN?CdevDIGr zI#?AucMU7cBl+ODFC>d2`K|5%*1@XSW2x(p#&B4dm+<)JeAm5u@*>9gnTJ`!Dgaih zVH#Kz>Vyy??^}r&aLFHhvu#nQQ+bl6^g&dYt^|iXsXm69|YJ&5IhP?f%i zDpU>h~nd?az9u-3y=>Iw6 zhr-Sy6jyt1L$JW z+l1Q}P1yVl+iiuKSSe(;T2TxlL~dO+oMJY@&KhwQ(nh99KilqRNit}ez|6<(z-KTf z*kv)l-P>3NtWwp|!5Sp4p5f~MK&o|o6N1SYvIB4y)AJVOZexZIuio~5wn7p?0V^4A zUF4ogOaBC+rKZ{D_$pu(DwYmbh>p9vGe+aV<1^>aw=u%*Z=n_<4&trY1n&>Goh^_g z(9&6i6WJYK>OmtU;(fDv7|D6KyYH-4vk>EZch3Kv569j!DJ+5p);PK@)P zRPVZHvt}6-xJFqYH5p-@-QIjRiG-O4=Yd&(-Y5fzU;i=VU|I$L9{7nZ*wFn zFKv$(@6BwHf2f33;hU(VWnzwy9Pnz498u`=7SpBMJc-BMO^q3X!Z&f2LJfTeSl9*? z0V{U%D}dRY9as2&6JQ1}v~4vxbdDbp>&_!CVtftpbv?|A-ToS8BgNRWw4LV*K8SG@ zG`_nTj1R;vC?X2NX{gv_#^6cOYUC}QRa9GDw1sgA?i4Ffptw85-8}>d?rw$R!JXpn zZo%D(yBCK7#l1KbDDBOE?|sVa85zmmYtA*ldC;dG9O+T>U=U%`?iwQo(N~7vnQQpQ z+0b|DT44FO6J+G%6>s$y;&5`!y_MP-svxqcv{wibm$FiN$C8=oS{!7M3TxF~_TRTz z^C)xia`p%`@b1s>hWr!9HiUB$jCB}E1n?%xDUDgBj+Xw<{Vk{;Yz<6;nkneR*X|{1 zNluk~{2R_RikA46eDSdn-Za)DrMVQbbU@8M*)vgvR1AIvB6}w<6p@f9R=JM*>7sNo zMp78TQFWTx`!CP?I`wX?^OtgEpBaJ?MfgMT`J(gkRr`O0U* zk47_;U6>{YUsD*r(9E;g^ycKFXU)X+;a!W$KkDVH^^{9g>Nh`8IWmigx4p4nSFd*< zVsn2gKX|CzXcs9bpGmyf@U&lg6Ay0qk>4s3$6c7-Kg)-hJ_ycsPQL}<-Dz2vjc~)6 zV*dJWA0-Vgq&xZOU3qJ+rD)Gty(!(0Zbzav328qaT@9g{(dJrpdk?-r*)L^j3@z&K z_E@xnVmOij8Qz1CJ;%OMh`OJ;ud#uGeq4^Fo^@WJV$*O_bU>=uuY$w$KOU#3R(B0OpY~sSE9}0{N7!9 zlYA)d0;ouOjQ@19!13m*RO_!%LG&WQ`r#?JkQ-vwXWw9jD2Ktr!ko6B%RK|Pyj6w7t`GTDm|84rEoEe+=oN~)bz*FF- z!4;%Ky6A=KowSVJ?845DoUpkvFSO!@yCtacF0=Jswb6)}waSCOzLz-;7aiU6jCV2s z&bf3atR$l;Rj<#JlPVN=! zaLrG3nKe{OcEL}Lw?YA@h{rfnD*fliB=&1P0w|c}mRDD0ST@hodY#8Pr^EYyqX*Tt z;U^TRi(Eb=a2bKhJ&HZ5rvHh=4%ZoCf`m3hL{n{7Dd?j0hklq(e$EC1-NmRN+|R!g z2|DXC!!esP6L!#`i@yTCv?!p^AE2Y)>*0%zTB3Ix%SLVzn!sczD8MLmD#G%4)VXb< zc|%B%-5`Bkp+Mjq+wAiSuY|A<7c(3WK=rE-Jz$28xl* z8pSpL0ROZAKvwrUs4O2wL?nEPC!&5Q?26ShVV9#{cl73D>x8I;h4388_{+_J$6Q zR1wEmu?MB?AaN+esc|#9nhj4+rJc~Eg5=v0SH;~h9%R5cdPM>c`ku+W-WZY$!w;HuLF*_?m()~&o2AiN zR%~-w>HB3yuCol^o5y`>P0@IE?Hsiz{_{PaV5_@f&0y`RpBlCm(OM~Xo~sX~`XA=& z&Y`gpJmd= zhX#L4JxoJrX5;fmg%i`N*T^hGI~th8*ZRa=%R;A)Gbv=(PM8Kqe@}lN=3g%Odl@VY z`VjAiJx@^?YBgY(g6kN^g!*CwpALd16S$+ z(=dRScMo|xiIjND zL+AT`SXzD5k*c1rs8JBY9MG39GM!Ys)?TX{s(rn6p=5HH_a*t{Vk~);p9S77Cp!Jn ziw8M{_6C+v$Tg)kFn>N{2HyXsKR3UIM|`1f@ewe5@BT~NHV}9moFvbRKX(gK_NBjA zxm860bC`M-po*RQMEs->oD$s@qyO-u8orV3OUW14Apaczm@6lYR0{!sg3Jt-%1AXy4x@Gxb#KeOEK*u_}!~+gD$_~9(Uegib$GDB>94|O>l8$5`ClOJ< z_{n&lnLOL64Yo9^YS${fHSN?QO~Qzf%*1KlqfK%W-hh!)YjfoiBQ+RkB?r$CclL7X zOXbui?#<85dG8%sc8UGGT5(p_!emMl|E6#kCc3(uZaVzbz1a5b@#ifC-eV_kPV%F7 zkIP_fPLANrlk`QbkqQWN5I^q+3Ir)O5oW7?ZFKo}%lmijb>jd=?dtu1pG{O7Mv{56 zMar?s7Y>$|L*kbPMKsku$fzK%opnEot@EpAS&+EGroa*+6f~sM4@UuVr4UL!PcU1* zp|0ayJ4%8gf_6W2qL5zkHE`kAD#(l>VG+P@GLTgxf{Nx$5FHbDL)PiKKhg&-n-?dM z$^J)(f%e-2nm2trwjQ>*25A|INyqKh9o@b9W7RTH*61KZD8C^dHg~W3u_Tcl=3`Yk z1uQaRRyD=|u5fukbAJzyWH${t&kXO8`N%n@>tQ zO+p9%7m?VD8HkD=JWsy*die0f5*=ganp|g=>{rvoRpmxgr%6I`sW6#osqwXgCCg;E zngLzsud8V+>YT`^6AQ{rX%@YpgPmzIc}}@9I&$yEs#broH4-qVsY`FEE?Yq-{7+I=}y4)vFCp@h_=I*qVzw@a_f`F<-&&W_lC1>%XtZwS!tl(E-ru6uE zCT=(N4~eUC=g7kPH|=fH95TEdG4Wu@Qvib`lAX9|O2{4oxDOj{wSF~^ge<>`A>s_N zk;Cpqm(|1(aaGy{N>zu5oPR$l?+#{ISj8LV(n>iLZCFh-Z5Fh zT9FhA5-afY68t%2x_Z__Ql;1$01rwKR2I{?@wf8eFZ}?vRC8V5l+oXMSH4%WwCHux zBa$uZJ=v^Cb{zWl-+g&=OrL5MSO+~Tp;N=F16(GBaBf+v04%nKSIt8rC|VU^N^Jf~ zTH~O-#E#>bI#B}vJrqD}mlC_q4P5zXcHZPLSZh6UcD0_iEspr9AOV;A2@U{>ZENyA zclj{3-7TOTY#-z3{+X%s!m$)in0>KS)fv}S@b~_&fN-kFCJ@ZFE5Wq5ATgWq{o=i& z->eXF!xtOy+6}m+# zBtK}6#*b1`OsC+zm$J4B_I1E$VvyIMb|vVRc? zT@Fwl|4%izH)QF=%7Z5AF|g#=1su4#M1MRv-&)}DE|XcDt|3G5AoE(gWL8ywmLsqufwYQoNVPt}?d9B7@5`Khdaxg~2S%IM z7x?6BIqfgAQ0v-4n6(xpZ)e9;WK#WPk!$VUGtCCi8s&b1qgG0Y1ye$m_{quvT;4X# z4SlWaFTujDMBhSLTyKbRXMZORxxExOwKA!_ysqo_=B zY|VlTz76soq47-*A*&Wmzv$H<+>tJs4}J4>ZB{nsChsqD(@>2EWLP2UJSa)`r4v3` zFkD~?w4sOyPrk<&6w^qUtt&U7KS2&PlOXQm6Yg{L9ZJG#nH zLcBY~{Ajq)_C{`!7P#5{QQtd2+6s^u6r z2%x9KA7g25DsX7&PoWb0xx;vlTHhO-asyxq@2xj7({-}rlsjD2=|M^qH%{Li@bGVM z{evZHcxyzoODC$$C1Ua7<2w4)$}`Rx&nFpReU3z0q7FvQSi}sX+ikLU z>LNn5+^d5}w9PVkdZT_c5ryBjdIvbCjF8aT#s91TwF5)mUo*T8(>MKuX7G1F_?PPN zibsh0X{(5ElhjL%P{M0uutGGlmKPbCWSn(y7 zt2JjiDXy-%P(|oMlB>}9>2f+rCKzHnRMNT_2pzPbcTVB0N8#Bc+>DUwG@g-#f#f-E ziRsVJm~B%Ix{@^-?VnrZs%Zy)|D81yWfixqQL$6F!cI+ib1;s|BMvtdPmNrOze6_W zNT-Cpfl=H;OQ1D)sNV)6hU;ByF#nNjr$N@mpQsc@rpnsqhDNa3UIDZ9QD(GZOGH!a z4rZk9vOazKD9Ck^)Zgu1*f(h4H4%0q6i@WKZ3KbkBj2#gL2ZQTjRS8W=8-9T z5LW-bC+$^#S=9JgU~d|6LHovnK1Q8ZdD)Pxiy2_RfHx`97`dF@Sm;NTJuLY1BY-Ig zzU!xxml(>uX&B@inD;w>C|fp_Jv=ZQ1sn~P{=EGzhYP>rNs3Do{I5YZ;@SUKuc2G| zZjIY-BC2t7Nd32j&;uuS*JZTY*qiSb=aTH{o3Kib%@ROnEWGz6@%8sN`aIiK<(NYX z>Mr2WIR|?pAirj!rKTnVcr#U5j7%C`d;8lO8$QgVRu!@>D|hbvqn0bc{HxNKETMGj4xaa?|)y&HkgL=9~y~No^%V|M>?cSDSE9_uo}4H zQYakowvt`Y==4SM0- z07e;{K0v-~xkRYY+FtGAYKx{gKe*WDTWKite3S#=;p>~rRlQf-yD^0JeNco{C5AF$ z;D3BaxHWlD|2v}eN$ifHhd$iaQ@jZuT$WxrI6}4E86=~I=;LD+PpH(|#Kv#|S{j3I zIoFK!qbN9Oo7TZHYrDVQ`*TG6!7FW^Sy`~Y5E+srkIywC;ieunm$$f6Y7T^*)RV2n zU>ObcrZ9BIZ|Tw@K-&r!RlZP3IVzjaX%>%`;@dGpVqA3Ee;gXPD)V38cSz-S;0oz zbqMzw406Nw#$xu;5<>}+p`+3?1$9o`w`3Ka^$eNf?=pkFW-K#Rd zYC!kd9aDf;3HeiQ<&z@uN0wI@I!2ndE% zcz%*GH$;*C=`n>D!Wl|45@~o->)sIamJT4!GTU=YOJpr&J+RH5slmhnHbs19mUdnD z{a#BCbV}xh!M-CGI*3yrUHl&pe)#mx;n(mVd>%Z@_+___RXpfN9;2-M;1b)tZK|4;h=aLm^u({atw72S;KRaUH=f{gd{VlpOvP zFa0YlYwB+QqwF)MAHb`-QemyHB+GOFG$)4av@o$B^Jjvmr;USK5lt66kP_b(8dRK# zg%pJw?43=p1pU!Z0mZ+|LDkcSRT?yjh7~S(dhW$IbB^D(=$17QtD00ozY?~S2u+E+ zr$z>!rKX^*F!)4}Df~CnG8soEY16&(AP}|Av_)dZ6I@}?_#-4g6s@hoT{8M%l}>Jp z5iyZ3Sev^ghiB_LiqDE&m;_HO-|88~s6?h@e_Ef1h72ovRoEEFM zexVVm+E{Wjc%R&+XQ_>$YMGe)UVZEQ?(8D$W0%A*0COj7+DjioNZ8KeVgcDg?NMF$4r?S63q#a;WH5fwx2o@Si#?cyY@D zwzR6fb98i(D*;StfMtnSXV`##w!l}3kMk4BhM*(SG4bq%4g7+s8}6&h1bm76%_(rD z`2E-YU1ihU+!qLmtQn$pbCYC%6v_s=A{|gliNQohlt0h z(loftUy^Si2~9dYoH#s8a?3(Y0ecjtOy`k^Dj6H#&jT9n(S$!@vITT)`kzCkp4ga} zwZL?q6znO+!2}zTjyc?aN_F`7CkgG-0%=oF2Pm%r`H+4KWht%(qqgdQFL^`_T2lS6 zQmiO7dqLt16_*9+*LgqmmC!aghs4E49|=C2s_C zJb#JZcL+dQa%Sj!wHF7XN^6T9kf?724F_tHS!2~YGW9ePhpLMyD!&-eKb@KCT8FWK zu(+uNS%Al(?ySKGx)Ra@s!BcObH~4@d8dgHXIO2IjWZR)&P|2ndjKW-m;M8Qbl(fUV;^|wk~<)5U+3FAp`i}3e0V_^8|ux<6?>ogK$N6pkeMw z<^Khik&&q8X4c(sYBgFUckC}-cOSN@a+^uaWod19v656HnKU?0np%`}z!n|oB%;8M zYJ;P-gfq=$N&E}zh-!~rYuIvg%Bt`fha&l1vM76KF2n1QBO7$m(G|JI*1Tz$1mfQx zB!-ri3B;12v%9jnuB7~m=!_IUEaTJLJLQF**758eE4u(&0PbHf>mb&~{1>D~uCCH3 z;PlPcpq0u-s>RpCb<*shf!qG&;v#Yk>%1DBd*E^q?DY`@DmvvB(y4MTldaG7aNXY~! z_YdfokZR}Y3g9;c!~PB%Dt6VZ8+Y4@$9!Ff*@wQ3L>&m0x5hoAv@eW@G{d>4a)FL$ z9Bt-bi8~pI5i`I8zdP>#s6K+GUzSC!Kb>-w9EaIWj|=GH?Ikzy+tKDhNYCxy27SLj zR}cN)oR*{(&}v$fV($555HsZ)F=oHlWL@a{^F*7Oem^a6$q*CwAC~ZL6uh=*Gj}LqS=n#@;TbkcuV~P7{q$1I9uu4}(W$YEUiFX`owwyPUs@E_r{9v)0ecx5Ann*TED-Fnk)1095R({0^I8rvRqzrSWn5G*19ud^ zzkll0dkw^uR;KfFLGW(LQlAA_gqzbqVtZ^w=VLf5-|>2xtFdQ3ebM@^`siT6bwgQ_BCGIExCC z2Hebz2&ahM$B2V8 zsfH+Z0v-PR3fy`!eiex>q~D3OqZGX~?!63H2$P*p=Hj|rmpd1AO>K_L%3)jy)3w2% zgXhg1*Do8U#-lo%E_T8jBYA$+j6Q5atf)G7~|nk?x0YG_ikL@klXMtfy6Psk#)4MHeCkB}u(I z@6=JKFLb46=S5GmfxcYPb)dnL0uCX&&7m^2U{~PAe0C0HYz-2qis>vU{2;FxJ6Tqp ztZj+o1JfFVeEt`Kyc}{60-NpQK#BVLB8Sh>lRnfer5OMVvTmN%ec)4o>_9GBedqRJ z1F&hlTa|rVKbeDpvXh3LQomQ@leBa?S>JuUxo zDxJ|L`~~5>N8WP?7+AlRMzpHdeOZ#IUK%iw2WZcp2)x7?HScFq{USkMwMd!yGReE3b#4Y^Te$Xn}Q6BRsN zHF2M#J1)#QBF)%H#{AKuib1H5iJa`0L`$+Wcb+?gC-~d`u#bhwMy)W;z(4<6p*_1% z9j(0^vb`_)`MQ5TNUCTzV(I=P#~q`YOd`$G<+g7L__4=+E#kNJ>4jTF4qmzk+sz}9 zvv-~d#ffJiYCX{ZDklj3vh41(y86W$#%!aXBDQiJg%|h!IRucF<+0Xk z*&~xN<5i^#M}nCzLeiU8IzxeJ_qy7I3p1Hdn?h%)mt2>9RHj1R5C#yJZe2-q!#J_e z7D^1U=~$S{VVNiE@-AxY`wc%Hrg6wb7gzi~>bG zsPth0H7zYP2*_&~tnCiH7{p7L)o=u;4EV#R4w1T0#qJm%oq-N+P^@a}1KiZJtyyU6 z_Y`EqjUuAmIf#KZOEF#LbgR-nAV;~NoZ@8B{H9^JRqAe34vK8YXMOR-TBl+6K^-2A zLe}=cH2wA>9CGtZ+QKZ+=02h+}}yA&dxKAyWk%Dp&ZwbnGkfQs!$DkoP6+ z{(AmRPF*wYZ%vHa`!n_F(Tu9RRQJ(T`=IH#ul@etzW%w(pnrS!oi9DQI6EDV|)VR0yR?v->Evo3Hm%mU-cabld=w(#d}BNfWio{h()l|`V}t0Gav zL8uO;gOV_7<9p%N5!IQ_5YP8+m=5YjK^7+GNm(@8)(6UZ`>EWDDhR;eo*MmJqe-cj z#_x-wShvS!#VOsg@9(c4czvkMc?OiOkXjE}lV5Wlud!=r;(c(T`TsKgb3gM8cn?X~ z!EuMnKSEKIK4>`QQr)O*p|a``ZX>d!Hf`0h(4<#arnl0Bc6*Q5-y*GAhrA18fmF&~ zuU_Aun9REd0M>&Z`a<;zeG^$Zfc#TRSb`>!8^q<6%=L92<_Dc^pUtnS3>*lB2g&mD z|2^FMXNxL6Z(<@g%RZJ3kIdp6PhgJ{XAX3X_U1482&)mRfSO%vh%-C2wMP#kZQ;wr z1fTcj`k8>%n17CmshCggBef=_AxOW6BD5phDR&~T{g0U(<44|DK+WjHhSy>`jPBqp zT&0?&rY2W;>B5-2!dqQIejQBp%HTy^+dt$?TaVD*BZ{JYnuu8(NsD-%$V{fYIe0N!*iZB(`aH0n;HX4dD)3exU)~2GyxIprky48a4a6{5X@o*T?h7< zJ)AHuT7K98!qLv%&rA`aJNj1(>8}eGl>ZF2wY`5`bR1aRu?ES`(w-0Wald_mn4Qc$ z?8G-|0DY7urZt-{idZ~oq*DS=d;Xkm51tp0zfKOlRlZdi2R& zuY%4rmmI>qTjl|V?~OMgJZ$bqkvM7xR743ZVI}CRQrY~D=Lc1=Aq;IeSu(Xh_)Q6q zjht_G1)0I6`RJ{LcbZyih=O{`Ml;KNC}}HueN#OL9oS|duhNcVuoYwN6crBaFi>GLtEy$T!IYcvaT%TcNA{PuFT&wurPKss`#iLecf5`kD++D? z_c>dgYPv+;0jU^{@6Bx^Fw1z4p+_fj&ZW8H>l(sTS$a~nkWsk_+m_02x`5iL)fC9T zd3og#Bo;IRx4KF0P{Kl6;Vv?eyvm|Kq*LWrb`0{BG;+V#a^++^<0F?A3cWm*NN&Xr zm55>c*ujZrH4_o!RETc4l;R6z+13g&Gf8_>5n!5`hHsr1?g*%)_b!5P!8P38UB4sC zR4XE}%G4Uzn)gN57bI;kxrV{^1-GtmpHKkG?tz{TGCg4Ns?srM6XMs5(o*|^^X^;? z_KP`UL1Z4S3|Hu%P4m#+W|ZjFf|>r~5#MGL`t?2_9AZQGj+u(5qeYoBDs|h1Pk7eg zS?W!)KxGINK2g)Le|~iedUHw>W@=MtBx1+r zxvRyodX)Z6^pD2+j)iNW`ZM)-h%7VTaw%(V^}`+BooqnEEzvwbu!iD%)Y$a-TgaDw z1DPm;;_G`0FWe^Nf{^I^qvM*COJMA{u_;G`KbJQuREDMu);OUXGpXw_)A%*>mq>x} z#~ny10qoM=b1_Nb;ZDkIvRE4{JOW1E%lSzho^!8hl%>N*-wDGLXvr4Zq-HKWA3i)B zjvb)=aO9k?-URqK=-?Bu#kVGO%?12*;MRd;B@jf_DmX~S*wAZNEuAG6uj8cDw=|j> z6gR9(WE;W#wc1XLpr6$lkGwiIj9kgTnu2^}ASI&at9z3gWlXqh^mmQhO{^}*0DYsg z3Js@w8J}KdA(=fJnAWt|;7ITY_Vw{6p+_ml*F>7uA6PAWxy${l& zxwv6ecq^RFRIN|VV-hrKGE}P6`ZXDRj*u2>z7=eb&g_fPH<3`0gMKBI#=`?8&a%<1 z%T;7?+Dn8}>Fsy#PD@(LGUXzYRY)YTEs^mYPx;9)mR)P(#xq!Wu+tfPIglJo&$sV z$${Cy3{t2)p6(G=t>y>niCdALmff0PX5NSn!KGF;yi186c$MNEWQ2OI8x0^Yh0tY0 z_Ak`gNVF@gc~5ZEp#d@6-zL=(s!F>*Q{^HJI)55JUDk20E#^$grZui>Q%=}5Tztg_ z27{z5g0kbLT*DVr;c?k86G+*7Pc`oaMS{v-`nd;XjS}6#eaz9I6g3VIeq5XDBai}RNVqm$vv=OSD_&E zSrL@sca*k&oUa6#metXJ#SW(C%W5%!N(*a9aPx;vi|I=GFb!)8A37fY;b~F-Qg_A1 z&ExAM9Pn^eCcK}Z+JN91@AQHNJyqPouQ%F@`H@~J99F=iy}t02kOO!xbr$s~MN*ne zW%ZuKWN*Ly@SbXlel{24%$iYnG0>=ttaidicNnMpRrVE*DrKp{WpB;2jCF5}O*dp1 z^K-#` zibiM2h_NErPky%`W^qnBAsPIFT1+3-7nQoR+@8NJ3@*JE&LC|J^f;fS6XyNU!#j8; z_h(ba^@ydKv%MUEcC~P zm+x(jDZ^-}wTX3N^B6sg&_f06j!qTh(qzamxo$91#Fs|JR1xkK))yB@dP^@*<8mYK zn5)UKk(^@xDk9^XXNbrzV8ChJisSTmz_W`aH^m6s%OQ$^xV!bVCVJEb#em^UH7$b7 zPZ5f2lb$`(Asp&=l;=E_zvD;Mzom_$ug@wI22?(3&#Upx628lX`L! zu|Iou)USy+$`ZkfOwHtOZb3!w9LVQm_~0&I=A70H*%N3v5*O>sx~$efG-LkbBK6l$ z`+?jOp^E^#jW8C5!O0f^)ltIju-^8|@U6H`eB8EsOj(`i`O&CBkzbBukr$`Qiq@Fq zXoiw>0)I%u_fK5wPYq{#OqPK(8okvaB|=r4U-xNnDu?bnh2UImWE2+Z$H<<>NRC-FI}@fJQ2r!Q}(x_x`ic^!jj5R5mxlx_p932(|;F>{qx*%$hfMSUJIC4i=D=y-3(& z%CPx;?=TV~OC6e`+C#`Vsh$0mbkxd2sG$CXB5OmBeO7DGVGH@ceQxcd0f=%?~v>{fSk#t{$F)MLb|2%Y18pdNB}DgLkz@e{Rf0S6&+b&R^#d zz0uz1Z|3&~9MkOsE5y-gG5NoAqnV5mZ4iVZw=U*9xO3b@6w}1VNCxB7i?}l%h58YI>3+ohI96 zaoy5PiaOw|MD<0o#y=4iDrVPbrTQ{rcaD&C4;&`QV$=;jXzYRuEQY|TN#F+v^`zNA z2b>Y#u0409E`cO^8IwU!wd1v>Ouw?WbS_60EXX`I=TedVwN*#tp+ze6+EM_G;0Pbk4T|`)GnG#a z5v<%?MrMc5WxGO>#vLRame9~JE_qft4;R&fV0sJ%%4?dNm|0j{ZdtzT3H~&DC7y$* zgo_(9EsInM=3#c9(HNUhW8*^lVpCj0bX0~uykO3783TORwB8|m@U(a@0O237$Efr8 zeZ^}6H>-teaCnF*EO{6wMNrMxR^C9MH4Dd^WKah0h6U+=o>{bMHfq*V+(Kk^!89;s z)75%{n^HV*Y6xoXDrK`axaHI7n%;jY7wx}2V%=;;1I%Q zwTj4i{Bho$k=;c}xr(qZH)loTtMu1w3=I=&lObFTC=PXaY9<7~O{vk}5u;i}3*WEe z&hi^OF$gPyuYBD!v^w;v!D-?2X8E2zGVCcfi2bTeO~yo(fn&t5T+PtTBfh^0xu7KrQq&^Q2<{!@*iBi7ec z6hbQWsKiJSqN8!zQj_pF_FEUV2vlLXl@x6xg#@lXy4%11m|KHI40cc;jk3w>`}CGs z3*m(D?0NX(h`=9Bfxo~z+#(rip>ML#Fq4c&K(WE17l&huR<|mr94v8#;Nu=kf$<(S zFC1=c`AY7?M9Uu4Q?bfKO1I9--B16vuI)pYmko}Doj-Qu()i%ib&oN~5AR2;Y!2<* zi6`gap%}lD+E#Pqvci%*l^doBDhmt}g50f&_7l_mPW~47=_QuNKCn~bsbE}Xu8#N4~g)Y9$9fT~d;fQJF z6nA$kLXW;rYUNlN@+HG~&jEJ%KvUuD5Ly~;d!wMs3k|%NsP0Tt?`2x`rA89axAjO7 z)7r+r0x_AbU%7PI^m)ZPwTrLlYv1{08IjXz#MP4wxhD9;-x? zB35WhXanZm@li2q*=*e0T@o$uqx&hrOuLEeZ97H3uP-XDDT|Y4K;Q1*M7hp!Q~0fg z+`BUNZ{7v>s=aG@*R=RT-onS(yu~W(=)0M2qb`ga@b^2 zj#g97Y>qYJL7gK(9RoM^qo0vgc->Dd=<_*qh6p)naB}5srMDJ~S~e*c*)pSW8!giWnnijYGOJ)x}zz+T~6+OeMb~xGE$CAUgyrjZgl< z5>kYRM|x~1cFI1c*<6I#T@V*S1JfIuhW}Tp&$VW>C^#A7vp9dNe2&{S92>ffrWm8| zGm*YT6!D?5|6rzGJ_nV#r+TE`JK*Hz#tjTswPaUgo)BB>#mMPUw8eV+1bX{l_jL+4 zFI$I=$X01Ev#kKL?3rjgOL)iEmY(IMo#mhf4d00x)|EBQ2O+Pf^fZ`!#MlbcG%QaP zF)KfXk1kt>QoivJ){+DutWV(NZg@D;>NeLGMPda$1x=m;iFMGqkC`7oI>u|wCSh>%ucRfYJ_CNf|JUTUZFhgCQvWL;qhf^Yc+#3WxLv4fiSB8Km)N^>1oXJ-D>-@ z$xzNaU``9&yL;zG&XmGR!LG5A4t zScUPs7`+=YV%d{GSemnjDsa^XaT^(6pM<tNdfGEpO_Afs3vnb}e7-55P!E8#SIR;c%xJ@BR8dnK>(y5Q8 zmX)c*1kyM2E4c~zyj|f2uzV43$29=wfVYs%2B*G=cx{PPXuDfJOlqZ4o7gmw5^?`_ z4Oq5o%N(~39e5|x(oz-?ZqB~(kRTYB{1`lSX-EuaLGS4On=5+IPYn7Ikk&1WU*+y7XbIV*Dtw zSiw?>KTn3J@JjEE%|p^!hxL`P_{{O2UO+3tVbasTZ1s(-SZ~8%7gecFH8{T^y_zBGNY*7dQ)$4E;ZU22z)X)MHY= z_}KjScPP(~mYP+@t?cVQNI~tzzvG3V!`ET<8sE|vIPxUI&yer^I?fW8IU;eu(J=jR z_s-1B9*b})({Kk`#2(ytNyNcA4R2-On$`E4fQl2L%{H}egb#+XBgMB`ba1Joi#0mT z6B}utFc*5XOOC*U2#68T@`tisCeB5Qv>4-jBV}4YXT%6SixSuTnNCR77J0UUzWrzz zK`?G;17MUMWfa+MF8s^o-TTUa^z=Xxed1wzuIl_GXRe@vsghdChN;k7w$NQ!$hr=H z+2j)flKs3x-Penu>riZg4J6kcU$+M}!LHMV&;SY^yDL_Zc?N@jfNc67U&lQBpWqST z8vIjdB75l5-pte9PNgISLav0KyxKjP<{XwFf7mfhs6~)yKWq8rf^g+*C11eta=asq z%a{IKL@-mUQAVqtSj$s3ckD58oVE$md$=jvz+e~bN$}y(C+HlZ%Mk%EOcCVFiI3$< zAk1l7;aGw6%LH_H|3nyYL|_;?zG2ExS1Z-gv5BA*n*&aZZIHj})hEgzbfxACbz^(D zQ+QjJs=3S38@=R5L}7lAU>KKGX(Yjlh8-TqiwQ8vTgBiu!>tmrs@S$C8>qBLU|#l* zaC+RYMssdsEq?rw7uSThccYuze_CY4f?uz6fKdifq0t+2#&71VNuLi}!elhVuhGL? zgkdBUpt~an)<|WxBOZ5nagpI;U5k%4jEamGKvbKhp2#{k%luFA) zNYq?}x}q((jn2kN9dgbjIs-DtTO%cxemGXz?y7~QjSCo4gClDft$1~%KZ$fP7Wu7pIG6}& zaJaEM84?@Rz`FpHDpAYt?aC%|b$MpYFd)cH_CKqynZp)$2f}ecyQ&B7Vbo<#!XtfI zrng5IKx27FkG@?v*|5v-WGqQxIB;&pu9O5wt3Tqlwi9` zeZ56gV$Bilm}-~Vvl_zW#VqqAn66-VnN2~0x2F7PXv*bW{?|ERMx1@AKpKW+1&Pgg z-FM<1@l`Uvo4)GQ5WQODs2)>1#p<>ER`2WIf|b*tqy`hh)tdJw59Qg#u`Z6)=mvR2 zrwU|5+IhQ9dbRQS@`)*)dO`A=24fsKoc6|8*l=3Lb>-4esgzVPoB^?1Oc%NYh^E3R zfU$i3_GhB~$}q>p;y1Js;}Zsoa7dDQU5PkecfP^dx19~MRy8at1Z`*GX%w~>^dit% z&v3-ln#whsIQ#xLq3@{NdkntN(&B}Lb|Z8+OdJu8Em)a3Cw0lhx*RL{%4DZA=;@9> zdQ{CXZ~(1_*70gEkq0_c&?32U&-|y$$VX$q9xvonh;6Kx>ckziarxsy2eMgeODEPc zO55$fv<+mxf_o<~q=o%=cTsgQ5pbW8f7Lv|sVUEl_@&Mh(W+kuFwSz#PFSLCWIb6* z{Sa{awL$UIgfMWPljyVa2Y?z`^V_+#%;?ByF)AaA4BCor{i85KO>ZVXq+o&l@tmJR z%BDc+AeV01+~x)G*XfFSbx1Va8r|o&yC^U9aI~edgW?N!`ws#AKs^K%lF=m_yFjaK zHD=?1oS)C3p-t^Jt-Pu)Pd-~gc+lGJ3X5t4IJzfN+o&hvnE1+ysyxBApQ@Z3=UQKW z?xm>k9H(A-`#KvMBGRCySuah56qCjGFD}>r9-tH2fL43_L5qwBQdVlLssT-SyG<=u z#_x2LKg35haL*)nj?Lp}b4VwVF>x?(b`-oVQ8t&>Sc z1?y(wZ>qmXM_k@Pdf&G?(Y5eT;r(Q$uxe-wsfQum3XXV;b+YqhxIhX(vG4rQi0`_0 z7jBQ>{%c}ev0SEhi#3OT3x{0A&t=A5vO!@}BIfO&Nmjq#tk_#}+~szl3~N$&so|JP zK#Pzg2A`AXYziwr#mp}0B{lRx!<)*ijF@H)7kB)677GhPIi^H$Av6@wcee3QYO-Vm zI`lhX>tdokk=nKbL9MbEmslo>-i*!{Eb!f)ZD|R;7MAX+V50dSXG_^vSUM@0X}p7n zlKP7$BQEAF`|h33pJA9a6!Z!M?Mm>N)bE=Oo28G2GvmsawvO`oq|px#TsPiY^mpQS zmRUYAy6!Ol($HBGeY{HtTutlweR zpe8FjRUEJ!ZR-zAK*r{mVZr#sIGS5997M2~^P8v%TG;$E_Q}`PFx!J0-+%&(gU?^h zpQ#6Vt`v$Tsou7tP&Oy;$K|8zDU(I8Iu#TQ{%8_bdKKVImS-mWlr)~=iJXq

atCNF<#X=hu(7lI7dXy{2hMuoaY>BY+H)2(cDXw#DKKc0zpRw6*=TL`_*sZLPxLN+m== z-iYynooh-lmyOKmr90WLwCWE883aVG2@>7G2PwIQ~|MrjGC^roi{xZ#*qmDZEKAKeA*ANl1 z827*P7!l{qrTBOE=K$mVX{EyhrT(w9P$UUUo7mvAbS>4liH#`7I2^Rlli{lPx-Qw6 zuM?zI*J`WZ`0nDkwVAx!XqXjkOe}ft z>}Jwm2~L34ipCdoCu2PZr{#HMorX|(*I4a_K7H%?aM{cW50>5&s4?~*uOfw7#FVNp zJ=nt7SNB$|Q~c=5Zn9{yw`B~*OKxIFa&z(I3E+el8Pb~7ScWy0yt*o7mFLZIYFC97 zy7ZK?t&+aiMLDev3L_y78SZW9?*5>a*j&S{u`fZ)%;pgjKJ9Js)IEtjFvx?%P$If$auk-cO_hZEeDP*=RL}u2U z2g#D^qXuZT{8)=N^lW5yXWIscr3QtgQXiD0e!lH(z-q+GR}JxWLYk9nF+=GpxcL6# zF5mj;bzih_M#C!B1BsDJ3#YIY-v$Dl9O+uZs7P=2E$P@!|KbR0lGo`0vbKRs^$m)h zjGs|Ti)~aWzNBVZ2scQ5e(NZZn@kKjYFvUtgA9H7)PYwD{nisHJ`2ZNc4XgQQ@QR_ zp{k;`K6tFakQm!?9tvlgoCEG&@kglY6mjblnosSB0%l%8QoBZSi3vjWKgYMgRHpR8 zIA29V<$}0xn<Rz9 zZ>sNk#Laa>v@R8XD?35WdTk57Q_~H(!m{9bfIQBBfH^KLYsH?|-=oJYx55{+|HIw( z|Ga9?q30-s)!(o52rA)NnrgfLN3{(Ezr+>nj^E+OiN2H%rbDXS! zPm*L{hQXo%W9}Tc7;f6g=|>JRdCkza&E^oPz+%(CEHT-cL}c-^qx)(L3KPN-CmsIc zihPJPL?q7m;kKMfCBgVn2ZA^L9@6ftlvlxoA%cx`es%pbQ!Bi*a&C0?<;d5*&cc{VRF!b8_Xl5S?lkn*c(@SMSXC(laB38{) z+0ycW$@0I4l~yrZv7TFg(oZZznuC9f$9N=a6G+}s{0Cf~SPu<06jBekni#eZ>2_yX z_&uLw3ipvuzx|1)Qr;dq=aD48v&Q1446C@S#)gr`*K;StnYXC@$NyrCzq28Esu9N& zQh}2EBN3k)m3e#ejQ^3>&vJ}E2K0*?cUWN7&wHE2y2VLRhLCclR4I7`5tJalqk>Ey~Y%%O9yx z`Vas167t47S~Vd{5C8~gVZYaRQs_>C(3;hu*F}RHg-@N6eX16~QSepj+h2LP5yv&R z%)tTAO>_wLe}}dSnEU!9rdD(n@ca$in#{M(jEr2d{`21pi z>3*h5Ed}WK#!?Otn_-^eTUHlsP;=s$J!Ple;lQd4?98LL&Vrm~VNla+63ekDW;K)L2MUPA7dcgZG?G8FJYBwQQ4GrA z?eiJO$O&lj9@Iar_%Yh}IyHU5F!y4YW@)m^Jv(BHWOx^3% z@<7ehTNkP1=9(`!Iu6-YrxaF^$s6^A64=}vmcxn140M)IdRNpB6knw5Y;%ZulgRU% zQqdVy?O1VI^uU2H29ehizxucFz{a9kG9t>Jgd?hZ)L!Hx^Q9Mu!ZZ$xG{isW6_(9q zkJ5_$`NlGO?bPpjOl-)2L)lwOc80gn8nn~g%o#WVk@X5piP40Bp6#UvjCua3NU%smdsvrN538!nxKf5&!xfg% zCAiN7k8`xNN=TZ5pFC(AzBT#gov(F43yC2HVuM+{ABWdpP8#y}I#RjwixH6A%AJq& z{2k+`RgM`;+*vkPQ9;rj4m^W_@ix;&bz~DjkkN{L*jG1 znm&=A?ieceiuI88l1N-9_hU^CM|sS*C*N)Zxhd$)`wwY_N5a=);%34aCC4+t7>K_< z2I!jRTG-0dZWa(159bG&6jCCKqdN%`tPIhPb4-&15mqwojKFu*I7XdMdfN#STtjtZ zVf~`GtX@FDmAZ>BjvaTs@H_rUa(svB4TY({nqJXTX6}V+V{1le@Wmy#HMSUO$M}Gw z4zYYQQ+d~CNgAZ*{d34O$H<@fLIQitj>q2J?wAOI+5w2+d5=K{PM&N2PS+2nt{;V6 zYNcF&s5i_DJ5uunoCVJ5V|hP+tpMY)O!pi3;)t5+>3|?`t-Tv8S@ z#q4{x7S;aF8IZ~HohJr?Y2rfKS#LkIxcK;1^V3oY_$t?&<8&g2$h>V*UPE|yD<-Ds z@iMB&eip(!P9~#KLT?Pc_=t@VCU&wGZvj|5)0^6v)AbnJLcRpdvy=|XhoG*dSdb7Ss&!nc<-{9;=9lH%MX-tk)nh2AKlw<+W^KDwG|Oi z`&e{~gt(M#Q-!4qG&`zlz<5o)k)Tf+ra6*Ja}JVP4gM~)@N^9>)T3CW_Zek16_+_8 z2+3ZTeyz7#2|5a^(X)_zy{{rx++!d@$%tb^JaG_s+XT?fa?R4?Gjq789A7>dhX+}) zA}&ZAbsW-gMdI2;tu?E*?8$w00Y?|lSx%cZvfpKuJqs|`0iZCE5WAn0)sUb5g>^zLOeaBWxQgB2btpn-77)M5T2mg%4{JPS3_4%UV`l zo~w)eD|ESIl46Qg?0S(L8XK<2td&|cqsH%gJGLkxItR}!O9Ta~5|XrEn^8fwfq~}q z=3Fwn6|@-EYGjeyo%uo`5~H!-EiM~BP%GM8TV?iVTR%8f8e=0wG#3Zha9{AsC*=*g zyr@vJZBxH*ufqnf6?9;LTjtX6(=*!NZI~x$kH)MKLN45+Uc2o~lUCsvSA5*MO(9ap zz^+jgq|yeozTUie9~;w5O3w#@W(w`QaC4_mhFYH5Q2emOM~Ynb0ag{-J-JQZ=L|Zs zQr@BMw=hEGHb%7U-Nu=ReWtZd|1(}DyXpea=)PiV@=NRYMThPPEC1S62_qNMF7!+OZ{D z{3`;Ldo?nIs}K#aEvR@*TC%&38qdJXw@q&Rm{+vCx3{w@tL;Yy!gNns(>5bPf}Tw= z-C-8TjKB+EVIFTHMG#~?kFI|A>4c5x{&nzKGMC>#H<@a1;G+RR$D@N4_X7GB z#55Nl(*NxWXmxKvoYO}4p~Q?PU>nGHhZqo7%Ud_!e$+hT@kv*FPy01bvH{bmQ`prz zS=h}JMIoq-8-Oc9W(OwoR=7eVRt;s?J!{{TCel_vBuw4~yF+pc8=}CSoIq#PY$Fh4 zC|uN9J>}XGUq3hDkA?p=S8PeR%(MxZ`rJq#+=mRd*Abwy9_5Sb48cp7&!PV#=0iu$)Lb2 z=t)0~e*d{3tM*9`ms%0s4W>lfMr~Y?P*EQswB~p*cP%}Q{$fcZ{iTKo6BlE)i>XuA)>k>t7+eHxgqx9ILo??dhk zHk}|Q(Pzf2Bu(pDeVfg^qZOu|jz>-??78{t4Y_#~pyNv%N|Q2l5(;q9i*^D_w{J{ogKf_eKCAm zG)jYW`f~lpT}rOkZE_l>Go?QC-#!1L88sf|?CO4;Czm*x{8M z&1p!Aj|@L@VN!~h01bdtz{^4+RvcuQu$I)F;N5Fa5QV4Pd9L6de*0s>iDKNKd*s&( z+;?1b^@Af{8~CxmQEq!f>cl8!J%!bnIuf$ZWX$!a&i$tT;sHLSswd@2s$VV*)WRTQ z)wNl2_MfU$N&5VG`VU4ni3;3PdgM0lR}fDPcE1xj>%ze&QNay^;%X+|%XJrf6#S9Y zf9> zu7&y`dx0w|Lyj_qFCq{OwJq^A&#{!#4%xobUdx|s(WB_2}y{OzB4Dc8H>21j0lpY%IzjZfjmozm2kwX$R zKW!r6?{B8wxj~9|pgMDh)}K?i2=p;{JhRp~dbJ|&R``=&ag-gAVMrrj54H>Qgci+{ z@UJg|F1xK(@}RnmIox%=!tAU$q|v0+>X_dsEH|C_P*)Y`ZC6%d=@o9|Ol}VSVZOx& zdz0=@)wxhz^Jf-LqiS5^(q=2nq+sQ8QrV~Nemo0P4+xmMJ2aSz{3X@PBQ4^skbY9R z-T4n`>*6++FMgd)8#wWb-C>WWaW{umZa5i(ZB*_InVQx(-jh71V@=#hspzU7=- z@F;wL4~f<7^%hsQ{8;eNRA|QLfqJoAu!D zDK3T)aNS<;HhpJJYbU(DR%9Pa{Sg5n_$Fb!h_!OhjFM7w2 zv6HL4(tD0Y{-Hmt;?v#%%rW`Oy4pU`knu=-IN*$&pj5A&GbJL^p*)l#F5{6|#w|)y z!eQUP2YIOY%D55xeRZS(Sz@E%N1>EDLm|KdP_F8?`2* zKt=xvXHh7TK=td1A2;X53s=<|V5T1cKg)C~K4Ofr;oG%vDS_rS?*;#AjLvO6WTinN zOO#}rJvizW3BOjG#4kOXYTzPI!hoKem6G%=y?53loT?+HrZ&C$D?pM!vv>Jj@6~b9 zTP(_#&U?LfAaLcHc(uZN)W51m^)l4 z!93T}HOC{1jq}~p8)-g~Hfmd}db?*m@9|>q`m>gD3I?^aJm}>yd4R<92yt3w@O8vZWA6-O^fujCIyE z9}z!;XuGXVS&5K-Tn8h?m9~Z@O>FDg=8iWC(+9pPXT*@4)jAl&w$?s;Tg?A!HN^OW zEjp5hm5KuT>R20#5MwU=?_cnKDUS8XUqxK^YI|*}{t2dB;#E?HRQcVd&dD8hz_(cR z?;L3_k9u!HYHYN#c|IyY5D)-J8;{&+#@PZdFX%;SFm= z>R=jGJn9hHWTYpjg!Jf5^a`GL&B!NVPa2DBaw_uak$C)G&XxBNiVhk{c14`Q>p$Sh zFyMgUd9Qy*OhOE>-_SjY-u!c6VdK&^@R0mbDxxBXOk@tv2~Ba8eZCw#>b?2XlUe44 zWr4%2`7`ZjT?+GtP`g`=TT><7urZ`N)WG{*gm{hM`3el_G!m@OLT-xzl1_pQo4XPXL0J;0; z^5r2_mT=MO7&Ut_n{YHwIt57=-vW<@r;qt2F=y5vLahAMHwtFo70y0Z+y{Iy!GQ|U zUIvRNH+tF<43aCjv^Zt#YcX|bWNF3{gn|07331drqbCDw1>G`0B=L`{vidUPCCC`( zc*78es^m$CsubRCzF3it<-hvEP8yj9SphDwwTOV?w~5%re8xD?I`H?#lHSP${>a@u zi!lXD0=4Es0?-#TIKu?LGZe3D_G>dC{Rdh{>Q5ZKb=xfo0cYSF(k=Xza#u7!YTW4jm$P3m#^Sfn7`4+5QcJlMsj}#QJWXqarB_2J|zL(OaYc zy=;3`ZUmYj$v3$4;4Gi$Spw*lYpdP8QAleBfDZcBGS7y7d4HFK6+?CL!jK!mnPXn> zj-w{!>A$;AwUS#x13O}9bTgy4ICss}{uD?fL4M|#NrgtE+dVU^TrWMA zyxJi8`!?b3-=d1^=qkcya2`6&`t%c3X8Y;wYWt_G4He*1Yi~uIqY3l|VO4xU_wUul376fsK(KQ%2co(;4yEXC*6)ldgH7fg5fi7Q#TWB zQctHAq-4?g>@x{Iy}s3Jy6B+oeNy9(@R}>7`3uSH z(040nZwR_fGX11hEOfo5PZ+lg`~=R?Q=8g)$bFMs?zc3_Hj|}vCy%dd(_DQ2^`C`m zYG_Vcg)@#wZ+Naq335Tl_qf0SMd{2P1fZvIpOfbgUF#&$%6c|;hL}=(a=ntHv(GAZ zGFA;m%DVmZ`_Zx}u`Fa(5hbp(3;9z&p}5R;^s#Eo4ywkn$eDm8cgPq${xVU!veZKW znGm)NFdn@Y8COdajYiXB2yz}lUUboLTr6)qk%~f5_>=9j4`YdC`onT9F!oZD%I)XX zvs%~E`{yZCMJijVAVeAuRc4|R%xsqlv%*DyaKB(I)0X>7j9iNcRs>c<&XG!%m{k40 z$g@bXwr0zxpjqL--On<*!}zmn?!JNjX}?9nJX;wX0eTgORdp&;G4C5Na&y^JPx&7V zGv>dIJj&d!=&+e%68yz7(#XU8R5bfD;$5M{+60DdY_yE=euee=8z$)uvO+43mj}o; zk-_O(eJ1{l9uhRh(<}FI$YN)asrYR&F*Sn?PFX26ISqH&Z11MMlv8Dy`0kOOV6D{v zy?tJ89gb0>=ZUxUnI4PNrBd54TXba~*G+G8=q-tVOHmrF=vzIk7ofXAM6J}j4waY| z)Z;dkMrIPpA22cwy9ZM92&uejSE)bw^d z$-vXdoH`rSE0Ij4Ok|@krCqfjI6j}1rRgxn7(x5;%H|KGXR@W>oTnDuum-#K9x=*O zFs;)YBw$Ov!&iDD+5WUOWS&8|vcpzsJ%BApSee;FlOU;GOIA;BUh&TYI%&V0rDk0k zfUp2XmKH^Qshhogce?Afv~Xvi)X&gvw=1YtD)rJQHaRSesAaba?SQO)%A(R>+4ZWtU#5B`Y%bHXixd#(>*i+=>8AVA*NYEcb)gL%wTi@m zPL%X>&BUN=#7f+dEFiZ9=vk`TmYVs86$WlrU-~P8=|lH93V!1+oAZ z+BGa}C%-QJ=}N(I;^9@!Mpp5|as&CYM<8H-gxijW+k2@HN+;tM~Y$9b?(e zB?m&iQIQ-D4I0dIt&o-BB9(?3Vksv5=BeC@y*dZOh-GAgwZOhdlI2#b?c^)e)tkyl zLaf7gKeY7?pt*NkuTDnepX9e{qD)D%eMFrsrKT!V3ZA0RmE}TUE%pi}w5S8PP zqPI#FT~aC|ezz%g{OeXtset1gjqLM$rr3lblMzwfZrnR1f+Qsx_1>^U$g&rytCO}2 zhv0;Gf6K(X7e;jaRi-wGlH4#CKWyh?K4n|D^R>Ur`-p50Uy2b_JZRioKjaV*wz8jWJKMAU!!Ky95Wx(neFNM8W}k5JwM!Qnk{N(^OYZROI!2rhNQ z^SVDLXjr>IV>N`B{y7B?z*lEX^82bI!>v&`m6uDRqYS4;rCO%t*tDe0^;=@M@Sbm< zyD)5%3J8sf>V2y!8p6HR&`Aa)zS-loHgO$&^+3IG?RQapba4?7##h)0#9HL}!3F+- za&&f=86PqTtx|=FVf*uDY6y1XRE1RbXP{gIX?L1HU-y}3!sW&2TZJiWCR}G0>5oKv zg)|*FY6^_4VacNLsWF__X5-edu8jN*W^a^k)>b(7_NhwN>X_axJzVU@%_<&##t>Avu)O=hQRubuNhSd(BBkjfN417s$e+A=wCg$ZM@&W zY}t2yzG3n|^*V-u=@S%-t{_#uhR8WAz3)23)vn^dv}%iRpCz{i`Ll01ZsJv4Fm0g( zvYq$xVVneZF@!w7W!PhhVPNS6f56Ak=tM{?p}~=vEy)Cq#+dW*i6(3C_Q&~y?@ngI zv#|viQZ8^A-0|ja{Az$-tco;TJ-UH);7w%U*2igba}|JAoYwM(-^^wHv;@Ox2?CB- z%&1;;&wd1_t~r&{DurA)<)2?2{p7%S9(S@i1I1dNG84rFrne#~9iRmBGgAdlW&y}L z8-YG{tw=*(nZa5*#CMnyq-BYh@qNRzd)4Y_{V&I00J29>L@wcxr|XxZv;A0|V8!A< zEsvh%z(hr%Ku}@~EUxn9nv=T-wcLC7^qv;~z%4hbbvTaR34}9jovAgY?~Z zdpjGQ)8g?$ANz8XuY%7nzN5Nz)Qo$@0$SF$ffpPRqYW_T*NFq8)}V>OO&nfI#Kp#% zT~X*uHG6!qIzU?N$eeAZ8aF|&OPwG{zHsaO@>2HZP93L}T3F+YA!i350OE#S?~_6! zSGaYnW0i&l@L_37IN=y}>)=#rP*DEy|x)*If?TrA2y%{j~;lU}U87;a5ZaJzm&HYVh~t zrN*a`2os#-#83i3U4JFWe;hC6BI>dm3?t#X?fxBFQ_7o6P#HnkOnM-xn2D8t#F@GL zfg$0FNx=>MZuSfLh^0=<2exR9AF(c+Y$n}b;@^Cz`=QXNLxfBac=sbhYS{f0VIoc) zm{XhGw-r`08YAPK1pRgcuG z7*o-VZ7xln(~I(aPUU2!{^xyD(=sMf5@T4UxPRM-Yx4g1bYC?+ouHySs1!FR^+Kr9 z>d7w4ffu4;ELZQYN%z^0k`@k_?yYW`Ab0Nk;vDyJT_I`Hsh|+{$t3^Uueuo@uRHM?G>8vBCUD=@`dHMP5B)y#jpCmfPJm^} zJc*CtKzI>d1opk{aBUHD=p>i#pR~VS@q#J^IS(<`1+#w=U!3(J!_pPU`);7v1wvAU zil)Z<1p?|@B;M%m4L@=cw@TlRlbTiFjp3ga4SRex<24F7Z7^!H7TvIgAV*Q$M_F>8x~>NaLozy<5V1xza`m^( z$u?=tkxR6a{7WxCYXK&7-NQOv=89 zg7`_lPaW0QpbR=2|}zPnj^7H}SF?dxjhLFO@%rD#C**LbG>dOwsyfNi$OO zGLwvl=jS9%eo5jg9t;GyV3iLq_t)shj&Zy3j!}EICyK8o4}RLZ&-btHkHDtE=MJZY z5*@CV*WsXE>V&3v^_a;TUa``MI}~OLIY-~9L|UQO=s5*Y@rspejZHE|D(2)wFP8lw z^c<#_2|_znb9nP_+ztQ}d7dN(;?|jFo6Mrq4n6Q$6V}t4j?`9s7FP~BQEBgbyK5D# zdquy`bx2{&zFpy2$UIKVSAybLK+_@qm)7dIc!4eNn0<}&isdPK;)O}$txRnp?#3ZM z<&s{p6nND7c5Kag zcRXNXd&RoO!oQW-zs@DCkBNZj{ix3|isgy0Kp=b4I#&Tpf``Ju`@(?N2Gx)(hfNaG z+eD`m!jZ^acb$1Qb? zA^e&176j8770avYR#rj-! zxif!k-4hP|_dgMx{LW(P5TO|n97(cPmO&gXZt~{C06b{xvgFc4eYn-!e%ImTMWcEX zwsEsS_N5H4RoP()GRG4*c7nJ#BYS)5I~*r%YMF*7eM7pdS2IjyTEMYI(%Zn?jAfeQ zpKEGERIZVwAtxh0D!;;;&{w0|{F3mq*YAUNl-TI!?kBX|HcmJ7$SCX#(J%dDIHqP2 z19v_kq>h)c55){Q_%Uz6*|_FhfTc~TrzatmRB@GB1xvbJgVqck;4!r2== zc;-p@k9(!)oJLN=uwlh_(mz0Ns=ipDxiHtK56X{OL@bMh!U^6UHykJ{bM?+3*(cY} z=7sq)Y2QB?Yg#;nwOxWRi(r&ePCjG9^Y&>sjoC{*XLkdAu_t(@B6ThC#a=S=6S2d z3`8c_=0J4Kv~kyUB_?IUCtn{EPd%X;LtK4t>t90vaK8d`;ZJ*Ifc8qwA-3D)7F-&N z`(EkGqMSCpHoOF`+tYdzSD&;vU|JY4lRj3dKhd`svL`soYD!(P+Iz(>EhIWa6}r_I z?uR?NNRpUZe1riIH0`N=MZ2VVt`W!0&R8`k|(IxaCQWm&K0Cf$9#ISfZ3H4fUZDYE(&IU5^)%*#C z+2K6Ph;X(?zQ$IPu1i?+9mqEk>*e;nG^hD%o zYnEJDBA+x4itp5#-$Jr(YMhl{nqW8;hK{MvCh1kKTw85yX0n6UP-e-)u_ov@1bFq9S`Ta_x91c-!zD2*svL0dfu2_fJr*C~=);fTynraerN zj1T=-a#_*9DX(5Cmy%{XqHHi&#$9~+Y2~RWjdwW$vB!eGapV>A5tBQf7Wcb0zN zoDyXef$!rnOg#lBhnPbzEMwg=V=D0L zry1f-_;|sz>_L}J;Ix9bCf3z;GTp9dhJRRq3h391PW#$jDY-fR9SXMQ1y>709$uju zT>(ewmL(G0f&_c14^=+ z6OY!(-F8Qi8|)Xuvv|t;&&cfe`%LIO$zaLWarzQNAx$Q+qpIlSUlQD@ze3V>xuoON zS6|%sdL@KqDyawr|K@$qJi$WWrck$ZHNo-oUmJ+(bD=qs)|H=qrH zfPEZ<^)r6)E(;(cn|V^~*M%_wLXU4zk0R~mVZOcS%?O%?l!y3>qqpcP(rUEJX@4AZw zgGl(S$%(fx2#q;y(D|&8V)?nHYiG2qSmtF!u7hq%VwqC``#NRMxTV@eJ6%`q&S~F? zQ?{aZCb4j5NCf=L*?dZ;^jkqb8?LeR@xel4_uURIgii0w8*Mh81eHcn`>lWd?glat zmLKkH9RM&?{v$RVYg@#Niq+{;kHS`g&^-4Dy<)Ri$0+UjgT`$B1tOX2!j zXr&%mIr*HD==5{&nf{1oB<4q3*OiTrsennT==g_E#~^(TONk$#k?7r#d(BG|0zY@ z;EgP8eJ4fSeDD6=eg9+?3SrpEUG2WN27)dO8COxqIjMFrV>@LtSbA4u%n zsZdC~9tt!ocEsycxh%iL{+)T$Yxb@XiJ*Q{7hT7Njjk^m(Y!m>=j)SexS*e z*S!Q79mSXYkvQ>ir;f+hFjuA@(bgg{=V*l^zk9Gdzvu1@!P|Qwbud&Wq%mqn)9Exn z|60+x#-&ZV4`2*UsEDE}+q<|(nhU=AGxc!ek%W9QtA@r!1@lP^JPmm`5pdwQ_*3sF z2Y$^7oyCg}{()dt^<}=ekcBVt5C7RJx`7-GHQs9h5=$b)r$x$v)>##3M*Z|o7wrJB z18=`L!WV7w%nkCYB=J|s{)dcj;YUyHPoKI!i{qE>hz}>V)WsUDWppI0uyBu#ObIlTCA1QMCrGlA_}>;{{ikLDprtPu|b5WvfLWt zf6a||%Wt$*jh)(qN}1{f?sd*LcoQmTVTfOOy_05IXpogb__3k+2!XlAbnxJWS+OA* zd8PKnU+q4)q^tuzqb0_tfSo_WxKcV@3Z{)qG z+5QV)so@{4Q|G)~b`IPgdiFPzk3A#)ClYUWeS-AuVoI*GQJZN>pU|n>6jk~YHva;w zC7ixe{exiT4iI>lX%iRiy&3MhiA$ulQVO#mX|`?u60@cze8=x`^bm$T8_4EoBMjVo4Ji$uFiNlbvCpVhrY!xRuV)}#Md(>Xs>{zqFs z+jgDY$;MQZZQGM=pKMz*xyiOB+s0IrU6b8B=X>vc{)5ji=e_q@d#%?=#aYFq@)Zp( z;oeT|mE4s|Yxl5gf-itGJNz`3oJvWdVe!yG=P3r&OAof_&Ht)vA}Cr*tcV3bY4dpl zs_6uBYx-V?#U8b)tD%gBX)E@)W%lO&D~B9cIoB9d+YzW~iB$2GXw2Q7M1xFY*r6LAT|BLH}~vY?hP&p^x2!^aF(2 zgb6x&`)ab%f%r-}5Nr|Zg1%D^!%ewBIfd$f3il=)|rm^_kY`s=yj?d?-ABb*?A)Ci&~xq$^v zVOqNwo&D}d=>>{J;mbC6|D@pV@>kuSy=zeD6YbwDF)3e$-E6dK-xMp)XD4Zdp`F?i zp9mXfI$iX-S4M(yXIz$pC0IB%#BpG_`YKQB8FO8jR>j-GTnJ87vtB5|%uV?y7K^=@ zE~_(Y(h5v;=A& zrpBDy@Vl_hF!gU|sqOXGXj392EB8Mfy1cG{Qd zjbm%N)I$}~3aMJd(QK!`y_qirxPs)W<>}GZX$O(P@^84c+{(#&Jd=)5f=laUu0gT$ zmQKIBSf!?mT}h07hVjNV&!ihxzSoEXYcHqAp}GOhB>boyPN$55Jw?|yv|CpOqI&$h z-G4#nPy@&~J5jt2ZfuV0I()5hSw)l0`+twyC7r9<-5(R;uh+V6kDEq zIJgBmqo7lmGTe>n_SL@1B?#^My*&@-jL-cH8kE~tW)$a@s%?BKT1>7KSO(LvgPr(M zii0+Zl$K<(hgptullXa5P$c-~<#E|*mK`KfWso4TRqVdjG#+psi~DG#&OKS6qI&-W zdzxWE{3H&)EQzMQwQ=yX3PS5ojG!Vewu`FOciqRhOk42~rRiK9HydhX)6lj`n$> z&YS&qNSRYa5q&~}hteey*>$0=eCBs~hF8HdRLgkvPa{CLw;|V|k-nvyd!6T|#BlUV zs5})T=6WxiQyPuQo8l6Sr#_Z--||%xvfdR|9df^)v*;dAke2FREQ+B|F=F;=GB)~K z$+-~&O-Mfi$RebFr0>Zfj6I&Ve-=gKNhDda9ZhRW@XZFGQg1D)bD@m$3-@aclop}T za{q+P4O)_sq_i=EJa4Do_kr&C~&e&d9XqlfN!-cBu?E%SJXi(k`p0gy zpUzas+;)Ww{7J6UooL!o5W)&}aO0~W`L4M5EaPozLp}IfuqhX6SN`(nZZqHoDqan* zVfE1pWkJu}E?CnxnnJkTViu{Kvs5{^?OT2fm7p;_Z zCoT!Wskc$;7g0{FEo93lWBCqHHIzmvPq`@fNJ~bvz;7>foa?&W{AoNjuH-kV5N{-U zE^0LJB}fwp3yeVlY=j{)Md_;Vl-SuJVRqGBYIu37C1GPFtJoP(oKf7f^6IW5f>TWQ zr$99{apx6P`YczhcbwL9=kwloed!6J=55&7T%UEL0SWU@U? zHPZLa7+j&i@lFE&z93-)hwbT=$hXdC>x7hqVy6vCdx{nikrosp8XN<~r3(=YHp&9fG~0`KXZX%h zA$(X&$p|(CqyBqwL5@b@jAamrbYLA1v<7lwDUB)6ep%qC5_pymV<>m}(fZnGUlo!h&^M+MgJ;k$Se>_U$ z&_4Xb@N$rw2Y|^?@~;Gip8B``>m!jjZkHD+D>-TfZcpezYop=-3y11ptLqK){cs(C z2ORm0mHceqK{ZKX3b#x(zu5dG82|OLmxmwR+Wr4Lo1#mxlC_JqgO}B4bwJegK;av52a`Q<47A=-S@{I4)LwgkfS1T;^X>^ZgTfa#bZw2F!Bd>f;xo!*+E^y36A zYWBq)t9t@mIJgm!Xj@esEt9DT4j$B)baFX{BQOA%O&Ao&tpDY?}$0#s^0osDf}xZis{Z z(~aDz6{WB{Y`3~hYL+YEbp=3inkK-z z%d@L=7cJduV4gek65VA9qw{%NA^---&z>v9%85fb0xYkV7*H!!S*SO_`I?(>+?rcN zA0Gq)cU3v7=tY%W6qAdYpMzD~O`c5{+K}SNGn@wOuzS}7pHz_xZ*9qhT9DOCSeku7 zRka4UqOOMPh{wD&Hd5P+kRUDF#LkFdmn1hBGu>`RbKRhpz1#F@g~oSJtqw`xaT6Ti zZ!3}B;YKMbvG|ZkPI`zehUf040LsvJz>DHmW#ZlS z*!Z=sDNbaZlA8jhd94E(|Q=0^LD{#4*(l zax#lkD@7?7iX_&v0P05~pmP0X^M|xv47pS%eY=GHbGd8VFIgt!T*U?B61$ETS#(1_ zBd3{cnszOK5i8c26nM^YgyY+wkh=S^6{!F?&oEHYF4^04l0s+J4;}Yk0bvRx8M% z%q20DWBEeN5R@>HTp93Ffmb%#hP$CV{^D1XH`6QZhfm9nMq40w`NQ{cCa|Ws-c#| zo_2W;Q|tCzEHrs?S})EK{i!GST?a<7Wkg576VeU_)U44irCa%Rf4L_#O;Tn3gkGQ4 zX-D%UjiChXda1QYl;?|raPL$2slG@27Qrzm9db3s0S-|C-r-;8gwQF?7ZkP0NC($) zNFV@wodRQ<0MQ0rO$?p%7VE>7PYzjru;obW44=nDEC>XUGd*&tt z4910`$&CZ0^9In*@p7nPP*y;hMOe8#~gPZ%W<0B^D z3v|{T&((43k+!;!@Uk!vS9y!lnv znICyMmjuzF8UFe4J1r%11-rS*=%0>hz8t&OhA?hz+D$N<*T+S~XNVG@^S&UQx$NsI=IoaiWaA zbzSwNMHg7uhMfNT#HT-@sqnouw}4TVE5 zZ}l|ifJ@AzHL8_M?^r(`M?bxmEdgAtU#D6Z7I(dC|8&JvyJYOzIp3$x0UJj&n|m*U zy^Tgs@oGQa+rRF37zJ@n7W8vV*x0Mp&e-In&a|=>kCIRgnS?Dyq@X$`7x6D!BQj0Q z<}E2Y{9*f>4MKDp;ifyR&;B~p0ijlm&S z?z;4$hqTbgO>JjFEIiQ#B^ibs0AD3@*j|T*z6n{>8)Ba-fX+KnJb5U87P1NSU+?c@ zUz`~zZySuAN{5eYJl$79s?uyuo#*wl#Om=B4+^z|ZKAc&c#HHj!SE|%B$FT(@7(W+ zkU|87AXfbN=4q4IvPiw4^fE|o%5#kGUL%%qL#I6|3OK7@?Nd#K^Zl;1^1eOWk_3js zH?HkeUGG!)>;5#UB2LSW>VoZd2g%auU8b1v!$;LF|9y=` zE(tld<+wbnEYy7GsI-QZGOH5o%@&8)HLyzsc_q(xZf*&t8J{wU+kqS8_@ne1%FaD! z@yk^=FDocZT~{(aT{DM>DsMcDA;8`799(f=UMu_DC5x{gj33zNvmQazJS@qWiUzm{1r1|CpMgbc~TNw#sk zF!lmYduF>4on!%eTZz>$+7&dF&JB@XEELI$B1T3!RN>&~GZyg+-h&IorSH-n*(hBn zGpD9_7IRNG->;70C_|d&39K1LYf;cavV;P=%5UsFx8r->imYYuOYK@ikB_Ml!ej~+ zQ&&p(VHwZk<#p&^HRrU&V8Qqo*p?^c<5P62rN_w1utJ-jPo#pYR|toz$|Ou}_OgdL zuMQ|`RkRfbpCrTa%EfRosQBOy>Qa`-(OY##+?~s#I50+wRx!#93BjvvD+r|(Nh2J_ zC^Y7KS|TI`X0$X8ftDkOkb`Bpz%^TzL_1Q({vp@JJT~k?Ka#S^y;|`*&qWWM&TwJ@ zoYo&}_~34VoijWIl>yr->H0hRJtVCm?7YD|j-XgE6@uXp2mPLqEjJKQD|AIUpd~~8<9~2OT);)@XBh8wtI9%3O)9rVS%iV;ccf_i?rl5ozZHmc= zOX4s7WCU4b_OG_+5PU5&P>gI=ubdFbfvya{a0~@%3lKyNZD$yqu@%>?G`5kYjjGdZjAgu;R8&dS-wTym@ z=ijbj6jLTLH@jN?RV3ABze+D7Sm6Bq`qKiuu2HFK%s z!DV0eFUx(m^@O2sDCMilMf`Uu z-j)kSqVOp~u>kdhXmGp~EE`OHu&p$qD>fxfUJ|KfS}dR%zLcD~oq?Bu%oI4J4r!77 zQ3$67-f5*{R!t9Z#y{gkBpsSA0pj~ZZ(<1fK^6h@kK7_(CXApf8rzg5C#2lh1zyLp%`d@a4qjnz7eF?J)NVCL#+8zj=K~ zzOAQ^pM60e(O_x?0PY8;i`ZwAtnp4-Wo6$AvbrWv`)TIqkkAzYs)UVkTN-eiR#OwQ zQcSK?jC-zk6TG1Nx}#POLG$&m!>~{jm-zZ4X9|mJ78WnTjwK!iWS1!qKvgXZ8d+ysXxgVHK7dt6t!x8H80&ZKqc#=+8ATqbz^v=RoQZfJCh1# zfFH^mS1fG72tT~iI-A&rE4$)j5Q$$nb)qW}a7wa7tQ8yn_+a<_5??4+JZ+uR3>r{T zx%>O-aXn}Z=9Y+)TWI0i6>6qM=!!GyBRN$M|4CI|Na@uY?-9^RC)`ES3XOxGiCKOr zAdxQ+!JSiaPQ|J_z2g_=L~R-t;Y8l(z_-NO70{W%tQ$D-{$gA|z~4bDG}4NvQA%4b z7S3|Z^{sVY`Z}y7vF2(%rQmy=JE$IJl_9ik@pT`jJClC2RLe9aZyAqH;Qs>4kinyF zZl1rz8F3uu+KAo)y#u^}#~0x`$q0Jh;uedhNH{2+Of^`zs|1b%l8Lyrl^Ohx#l9LI zNibd6g=Yrs+2G@*E8)otbfiUuMdBpI&+%SOcNSY zcH@mu&^B^Eh**@0UDe3xy3tUK#!-0NOw}E?e1fkFX<4Mo4E$ZT!M4C9y0{;7X;DeE zqE$n<68bof2tjRco0~H}y}D61ZUIw7N={@6xC;T_hD`Cqpr-&gUfTqhHRxJ0RJ^B0 z=H`MAMycai29ZdiphAREo%5F<9+w4JHHX@`J3DWIE62xUH>b)6>WC4=Q9d6;Fk6pn z+$x9%t?=uOR?3xa>#E~MVv#x6J`6XqhpmOk?#8cch!U3bL14G)OeQBDh^@&gmSl+; z?xWFW6J`+tr3&FS1$rbBD*H~I@S75G-YRN_wU<;Y=E%^2Mtq5a>VLTRp8emRR-$gq zymnTiv(=Na*Z5<%A*a2XuzY(f5F8NiznvKjOM%3mcB?aT3|W3z-kebZN4ATtW|9CQ zv)xOEYncoz&1+DVqw3c*3{3scr{3D&eqdcM6z*^YZFW{Sv^Zpo%CXHtmdAYMOke<( zsNyKdPq{F5_Fy11>5uJR6^giKe+~YPN`}b3ra4NU2~Bwn))b=(YUAOyW=Q3nHiOE` z0rZB9Fc1v-3!vlO?DzE&yx;BNoI+(OXmaM@a>69lFR zSVT@#8-6Ely;@o!%B0_mzL&B5V_rD9rBk6vPO+0K=<%q}6p}(mT5{J9mIiTh)j{7l z$>B@qMpc{XJD4^t{5=+`xBcRDpyEZSogIl9(KCw3`@7oYfZD;8Dm5DAMGyY9VB`Dz zTHTj+eM6!~DRPExUnk^TM;E+*Qw;oJgQ>7v=UuEMl1aBoPe^M@xP}pNrzJF9*lyE1 z3H0(kF=Wh>m}_4qkY6RJHU?I|GY_8OdmLV4NRq-;j?7$g6fAw;gh)=k4z|{GmXfn> z(d{?_BOHz@-OCNa#`GJQ_^WWj$(Y!SyQAxTF9Zdop$y=GRCQJli%`WN`_uxp+GV0& zUB*v6tDj1g*5gm6deX{W+D?APT&9@K77A1Tt@FOQbf9b0 z)HUYz!a2uN7jFf{=r2X6+jc4w^bl*+ZVURp0rafOrgLZ!SLXzWXnV#vb$T}2=`fe# zzA)fQM~=`T;bww5mrDX1M$X(%dfMK>JOk~hnJ>bnMEtry4E2WFZcNt8f3%gvVjSdX z5Joqnlkpk4Un{^6kaJS*B%sIN)#QORP64G~(z!*$G>jg5iZP04Cl|i?v+EUkTmXnW z#sQQ$BXE)!y*(cmSymPQOVfw)182o8w^^n-pjSzL^d{kF<$;p*76+r%&I6ogW#O) z5nrBsx(pTTb^!Os_@tm50j9-RKv&x~eWp+yQ7fQVSHLfC*D~zTK)yR5oY?3X4c_5DC%$WXfcnvf@Y~UoF0%w zrb@qSxkguDS$HGs|5U=NLn|2~sxilip_izR!VWE+UN6j!xY++(?&fb?BOppnjn|N|YgP$YWbZ!J(h5G5)sV za55O7(}iGdsq9f$$lJQ|3+4h{!5lT~1x5|Dzktd2W-j-podDo;T0>?21zNYR`8(ty ze*R5ZhZRnX8^cK1X5u2(Q`BCDKg~Dz(DE5iCzgb@bG|NoQaa1+nZGB?$>Cfv=i1ik1yHY%#imfl&WTgz08A=&3I|^(D^0Y2&Kcg0s&|=;v=xgGHbvi1-g40Q2 z!;)O1aPJRoAJ*R3p?8I9DLVT(X!G3Y3oWTt#V5*1V91JAJ^gInJNo#>m8+!3(1!V| z9EjoB?VPNrjES`W=s#935HT;$LahpbVuHT^xus)S>ZW zhKInJnYxJYA4g(9RwRQ>y3iU<$HmJv^T$8$6+YZtG!-Ax^MG=Zgpje@M0!Nd_?_AQ zeu74in)00)WQBofOeqo;*ITGJ$fE=a;`&lw9w%h`8z(7$O7z^`c5iq#8Scyx&{CB> z9p<9&PStIUbm<$c=i)Ww$^o%1->wsMX1P5aD&c4cWGlo2M`8*~V?pom{;!|L&OI9@ zeE5xSG$B)(D*IEve&V=fv8-M=?Y~Z163aC`5#+})ODP?vxlx75B#Vq|o=Gl*2AxqL zo`+AQg&Bs~g){0j!MfM+WI5xSftoqEM&!~QJe_S=>!l4&PGK^y11`u6C?nvxIRfY@ zW1kxCl%?}%g@pWt6n{jnDbz1~5q02?J@_mW>K?R}F(-qmI{r#aPPe7Nw#}wO6r`}5 zEL0V|fS^JAQ(92{333uQ{HYJA)V}RpE~*q*`ORVM4~tPlj$nUFwh7!W637UyR4(g;I$urnzpB%v8(r`EE(-_(SYspsYQg`hBBOw(QdxhzDC9LJuQX2aUKu@FsU8(n( z(>W%c5c*$7c%)qCMq%151_<)Aes^itVe|0H-|@j0FSDS+j~yBh@BNHbj# zgedemLU4(&$;&GWk519>z~cg*x6Z}sfx+>ncWR2Fj>&!!uxpetz-w z`%xNUTZyew=Bc(kFSf9(@%in;!OqwF zc{TQaqydS%s>Ue_So)lVS~&2xaP1hgq;{2=^}ne=)+=*Yx>Cgi#HERjs}7PSDsc*Py+kc=k=4 zYtw2~Ku8f)p^_clX)jkOQf2nn~#X5atQr0D0%%b7*jGG!zF8ZwhH$~GjUstkh(g?)OF>#~7cHC5T$%+J+v zUeM_f)MU-`=ITM50(IF1!6SQl$1Xwv_caxo=QlM7ARt9b6%S1L>A=(<~$&Wh#S=15-Bov#o6$)0=bP z{k`UfC-DQr&3DysNUS1Du8hQu`f~0XWB9fnl1Fz`hRGYhawr-&-Vb5ct)f1_3UXL# z_QWsmPWTpMi#IlfUvRU^R-sl?OugDEa#*C}RiGCVvpwK+yMJmq(4}%Xg;FJXq?^uY zwIG+vM-8FQTL0Ve8?`5`j)Ej6)&a~0y(;M~_48zd<+<+fIJT265@I=3JePWviMPOV zU;Lw}tA@2sD5EkG%~hm-FLQy0a-sIH~IgeTrPe;_lup1 zGy0)q{NYzI^K6b?4PjuO8whRZtJXGTm)TC)MpKEldpxD#dH=CrHV~O4P}DOA%kWcr zeRMeU`PqTLXVf*C^CyUSl7`;T`9dui+ZS1u-#p!XUN~r!`r$N}k4J|g_Sr=~oq`>D zi_X2exD`TcR8C7x+&Y@VO>^;Cz1@jp2v8hQfv;1`@s~^|);>IvSZDFt__J|9;g+#l ze2Xob+VEiJtS;`W?#q+yU>C@lebZJKgR!&}2ZupX z?YJ>v;zc=i8C1v9eGUGnx@-~IkKj~-%iL0%v5>{-F4|dlCt4=uGbf!MCX8s-2s=Y% zGumYI*r1%o`>f#@ed^cnzgxl9h`X$mhh7ggKikmo8Mtuvq&|BfZ2JjVe-JE0Q!7Ly;LUobROS#0q0ug$p4}p}#E) zTXj;hGt{qK3bi@9@c8GrxC1X0%8s7I#xC3%RN=E_coE2e#EJPJkFkJK8^UIZpg^Zn zgkH}g@cZG;b6)5i$LJ+SGSsTatQ zBCEyB=}Ph*;-WSB$EX4LN?PnoA7v!*0)kyUYAW*MHAzu~E?YmBI0!Tl)6L0fe7kqs zn>zWOmx69$0P>JBF?nlQ($TrbirZQ}zf~*r5351Xs5LK%vrd*>z$K9^q<^_d|5h|U zq`n7T!DP1evF}1D!~+=9v0!SmpSDZ483apuymCGcJUCdrm~bM0o(}5Jnd2af*;DIh ziBkKD_{dEEOT_sHoT@P^Tfq>M)%A%K0q$=*`cfRqYr?5f9BTykPb6_L*eYkKk(Hj& zMa+OKb<9E}f4j}xx*b3Xh-n=yNBrIhH8ObtS`jnN|0z^?&NooyXWHf}N7HpUgG&FR zPpIOeHmmY2t^78z9F-j<_u`AYXX6c{$m4@TNh<&gnW;|ClG((kM9e(1vVw-GMf0%!nN2ce zk-X_kpndCc{ zT#aI-qujsg#%ibiV6;|g-sR6vX*YB881k0?m^nTTq~WPXUwxFkpeR3l zC(?7(qh9sKV2j+SXMmR5-~P}(Sq3F2ec(k1ky(9Lwf;4ok!BXfFFpk-3a=e=LBoc; zfXd)RQmn`8eCyH`=)pAYqg|r$rQSN2qAoZl z*09ovQd?hhA}V@3p{DZBnfriI^j5L3#H!0fmm>=E=3o2XU$!Cazu=}IeLEn+Y~jH_ zaJd#Sexo?5uKu-s(;xy0TH*^At>J*Kx2h66MWf-_K=5@yRA0<~go(SwpIySU z&Z|&OhOJhIz}0xUXl|Z|&SWWygo&HG&5AN}=4BSxMpObiYTEL0hHM*eQvAfbx7nj0 zESNA#H%v(2YFvx~=aNgKx;#q%R`!W)xQ`8#OLLZNSCM9fJ&z;i%LmuTbiQK!M#wqI z=a*^xpr)l@Eg$|+B^CbSxiH)K8itFoG)@S`5-l4M189(Q;NAVY{c5#_+7rYiaVVaS zeEJgDCByO|X8KdjViTL4;g6cma@XE#gI?dwKl#W66<72V=5`3(Pf0+kA*J8f+yB{( zMa!(;$~vp@d*=9h0B65{r%7iLfPGr*2<=U4x?Ht3a`Yus_J0&h3?82dkUvvx^M~1d zx%Q9}$hxC9+-u&pgSy;69y>x0=LPQ}x$|w*Idt-u$)BN#HC7#qP^QQJaY;gjJiN2F z2Z)Im#AcG1$=n4sU^wG(8F$)#Qt=VgQs(+9+Yr<_v82JQrr`?8_;n z5_+(ZA}Me+1TN2tKiC78Md?>gwY)6MGBF@CD_`FEJ`j&>Lt$5mu;BJ|k6D$!S~7yB zcH*8%b_SJ#UN}|_`*H+DGc^Vt<)S8F8v$U-C%WIujeEAB`s*YT75yqd>pCh&UFue7 z+#qJ90zWmOlX@pQ&_de4(T{Hy#M`}L;?a`Pk+6x#VkNU3dVbX~j(O8w`0)7m={@nl zGAvj-BB>2qUqM$~wG-0?!K8xRWCrx^fnG0(&|6YRp7?1tmf5`)@>DoLARY|I@iz(T zU(4}R@zFo96ghWaS6L>?k{Y|hw(^mbkn6GB=$Z9!t6aa1Gz-y0SVowm{8x@zL^=D_ zB1Nj+S9qEX>>$<)@G`~?$-9C+KmQgFN>s3R7{{~eW62xTu0y=tdGP3gdO?9$<$=8l zPfR?!Zb2TWY@P>$XJ!ec%xYwNxRvXN30WqOFqv7w%HVnRB?D0l4;)oQX#m_Ul;jx@ z4*zCNOtl$2oTqn@2sF<5iJO&BRv;4p`@E!{-flV3)Zc;bzH}OH11f?^)wj7^D>!laHu;3emf26eqXU4Uli1L)cP)WRQLf+M( zx4)E0ojROD#6|NboCE5VJ+C}wt^sjE7)tH}A8E=&py*2R_+jLQ5-2~G<0i8)Std<& zjWgEtV{vkS5o%~}+(=uukkK33?z3~^a0X7QfZDcRm)02y=kT?y^Tao-5n=NTH2Rmr z-%iaLH6-zS(ybaBa)pNyeN16fW&z7lD!RjFjcO;4PEs=jiJFCRzg_`WW22wAl9~ua zuu!#i?vc;fBNy220uhJf>AVwG|F2SY2(-(J>K{BzybA@{Zoe?&5LY{{a>!8%(e{5g zwVn<}ZbOoJ5YVb1IV7R^*np;^Gp~0B)|3IEJHyx0i!gB`FDZ?Fkdu+WJJrtEw-B1T zr~=dEVp}xJ^kQVL=utU%JU-`>axu^8s@-AjAJx*b{(^*cWD&&U!=bU<#wLU!i@4hp zeEcr|mRzGqo=fJ7c-}()e9X(0S-!A4F%8`jDsb&ght&+3>WKM3)GyX(T9{sVMcwML z-ABnJvntGM-ZlFXu2+%194x@M*wvAt6Z|WK2)_Nfla5`!~287=jva} zh-F%4S-2j3rYUNAAR3ST;#DKWBF77KnF=LdT5!~qgqSWi8OMPqtX(S{jJ`<#COdXa zv$Aa7u*m5Ls%a^)M(XmzdXQIeh@M41x5}6pStY3PlqkY|r>z={6BCvD&Tq##Fm$CtwKS`cizkS5mV9%oi1H}jrXjS9uVf1PoF>q`z;{Mu`yECA$;8ifO-lNJ) z9M^%_)FLIShe0Y+F#sOzSa45R$S z$tuXHsjI}k|CL#-Ta4S!Tu*_mPfw!R61i{j9zu{1l3uT!fnADFow3CJwgeFHwOh<1 zhnco<-NYFVli8udv@k2y^MeYlxFq0@>stowsvMMK-B&Ag{(4wEFXKbU`N7S$RVU@d zrmvB_j7(Sc13}cDFjL1yPmp%mly}TWH$@`C7l-tdOn|Ad{5k4Mu4sl(Qu24{u)i=l3fl{G(w5S zfbs8RMKow3GBeBM8?#G>ns|KHP>*m%3)(J5(-%E0ZNgZ=T`Ow>hV;s*ozESvqEAHp NJ4CM=j|g~({|Afl)R_PP diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 3434e1f80a7cee..552142d3b190ae 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -126,7 +126,7 @@ "title": "Dashboard Foo", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, @@ -156,7 +156,7 @@ "title": "Dashboard Bar", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..7f416c26cc9aae 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json index 7ea63c5d444ba9..c99506fec3cf54 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -11,6 +11,12 @@ "title": "[Logs Sample2] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard2", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..4513c07f277867 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search2", "name": "panel_1", "type": "search" }, + { "id": "sample_search2", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} From bd99b19bd81a635da4a81de2cfd7f36ccca24014 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 12 Nov 2020 07:42:41 -0800 Subject: [PATCH 4/7] [App Search] Version documentation links (#83245) * Fix CURRENT_MAJOR_VERSION for use in Elastic docs links - Was previously just sending (e.g.) "7". instead of "7.9" * Add App Search DOCS_PREFIX constant - follow WS's example * Update all App Search doc links to use prefixed URLs - except for Enterprise Search setup guide, which should be updated to use a shared URL at some point in any case --- x-pack/plugins/enterprise_search/common/version.ts | 2 +- .../app_search/components/credentials/constants.ts | 3 ++- .../settings/log_retention/log_retention_panel.tsx | 7 +++---- .../app_search/components/setup_guide/setup_guide.tsx | 5 +++-- .../public/applications/app_search/routes.ts | 4 ++++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts index e29ad8a9f866b9..c23b05f7cdb3d8 100644 --- a/x-pack/plugins/enterprise_search/common/version.ts +++ b/x-pack/plugins/enterprise_search/common/version.ts @@ -8,4 +8,4 @@ import { SemVer } from 'semver'; import pkg from '../../../../package.json'; export const CURRENT_VERSION = new SemVer(pkg.version as string); -export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; +export const CURRENT_MAJOR_VERSION = `${CURRENT_VERSION.major}.${CURRENT_VERSION.minor}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index ea4906ec08946b..2b96e3322cd555 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.title', @@ -100,4 +101,4 @@ export const TOKEN_TYPE_INFO = [ export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; -export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html'; +export const DOCS_HREF = `${DOCS_PREFIX}/authentication.html`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 23572074b3c692..3297f0df4d7bd2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { DOCS_PREFIX } from '../../../routes'; + import { LogRetentionLogic } from './log_retention_logic'; import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; import { LogRetentionOptions } from './types'; @@ -41,10 +43,7 @@ export const LogRetentionPanel: React.FC = () => { {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { defaultMessage: 'Manage the default write settings for API Logs and Analytics.', })}{' '} - + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { defaultMessage: 'Learn more about retention settings.', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 60d7f6951a478c..b3faa73dfaed6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -13,14 +13,15 @@ import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( Date: Thu, 12 Nov 2020 16:49:47 +0100 Subject: [PATCH 5/7] [Lens] Add suffix formatter (#82852) --- .../indexpattern_datasource/format_column.ts | 16 +++++- .../public/indexpattern_datasource/index.ts | 2 + .../indexpattern_datasource/indexpattern.tsx | 1 + .../suffix_formatter.test.ts | 28 ++++++++++ .../suffix_formatter.ts | 51 +++++++++++++++++++ .../indexpattern_datasource/time_scale.ts | 2 +- 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts index 3666528f431667..1f337298a03adb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -116,9 +116,18 @@ export const formatColumn: ExpressionFunctionDefinition< }); } if (parentFormatParams) { - const innerParams = (col.meta.params?.params as Record) ?? {}; + // if original format is already a nested one, we are just replacing the wrapper params + // otherwise wrapping it inside parentFormatId/parentFormatParams + const isNested = isNestedFormat(col.meta.params); + const innerParams = isNested + ? col.meta.params?.params + : { id: col.meta.params?.id, params: col.meta.params?.params }; + + const formatId = isNested ? col.meta.params?.id : parentFormatId; + return withParams(col, { ...col.meta.params, + id: formatId, params: { ...innerParams, ...parentFormatParams, @@ -132,6 +141,11 @@ export const formatColumn: ExpressionFunctionDefinition< }, }; +function isNestedFormat(params: DatatableColumn['meta']['params']) { + // if there is a nested params object with an id, it's a nested format + return !!params?.params?.id; +} + function withParams(col: DatatableColumn, params: Record) { return { ...col, meta: { ...col.meta, params } }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 92280b0fb6ce6f..793f3387e707d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -38,8 +38,10 @@ export class IndexPatternDatasource { renameColumns, formatColumn, getTimeScaleFunction, + getSuffixFormatter, } = await import('../async_services'); return core.getStartServices().then(([coreStart, { data }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); expressions.registerFunction(getTimeScaleFunction(data)); expressions.registerFunction(renameColumns); expressions.registerFunction(formatColumn); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e37c31559cd0ca..94f240058d6189 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -78,6 +78,7 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri export * from './rename_columns'; export * from './format_column'; export * from './time_scale'; +export * from './suffix_formatter'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts new file mode 100644 index 00000000000000..ef1739e4424fa3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormatFactory } from '../types'; +import { getSuffixFormatter } from './suffix_formatter'; + +describe('suffix formatter', () => { + it('should call nested formatter and apply suffix', () => { + const convertMock = jest.fn((x) => x); + const formatFactory = jest.fn(() => ({ convert: convertMock })); + const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const nestedParams = { abc: 123 }; + const formatterInstance = new SuffixFormatter({ + unit: 'h', + id: 'nestedFormatter', + params: nestedParams, + }); + + const result = formatterInstance.convert(12345); + + expect(result).toEqual('12345/h'); + expect(convertMock).toHaveBeenCalledWith(12345); + expect(formatFactory).toHaveBeenCalledWith({ id: 'nestedFormatter', params: nestedParams }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts new file mode 100644 index 00000000000000..5594976738efee --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../src/plugins/data/public'; +import { FormatFactory } from '../types'; +import { TimeScaleUnit } from './time_scale'; + +const unitSuffixes: Record = { + s: i18n.translate('xpack.lens.fieldFormats.suffix.s', { defaultMessage: '/h' }), + m: i18n.translate('xpack.lens.fieldFormats.suffix.m', { defaultMessage: '/m' }), + h: i18n.translate('xpack.lens.fieldFormats.suffix.h', { defaultMessage: '/h' }), + d: i18n.translate('xpack.lens.fieldFormats.suffix.d', { defaultMessage: '/d' }), +}; + +export function getSuffixFormatter(formatFactory: FormatFactory) { + return class SuffixFormatter extends FieldFormat { + static id = 'suffix'; + static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { + defaultMessage: 'Suffix', + }); + static fieldType = KBN_FIELD_TYPES.NUMBER; + allowsNumericalAggregations = true; + + getParamDefaults() { + return { + unit: undefined, + nestedParams: {}, + }; + } + + textConvert = (val: unknown) => { + const unit = this.param('unit') as TimeScaleUnit | undefined; + const suffix = unit ? unitSuffixes[unit] : undefined; + const nestedFormatter = this.param('id'); + const nestedParams = this.param('params'); + + const formattedValue = formatFactory({ id: nestedFormatter, params: nestedParams }).convert( + val + ); + + if (suffix) { + return `${formattedValue}${suffix}`; + } + return formattedValue; + }; + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 7a4e8f6bc0638a..06ff8058b1d09e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -11,7 +11,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { search } from '../../../../../src/plugins/data/public'; import { buildResultColumns } from '../../../../../src/plugins/expressions/common'; -type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; +export type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; export interface TimeScaleArgs { dateColumnId: string; From 4932dc55a6f4e97690a5b2d659eb0a854d65cf17 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 12 Nov 2020 08:58:05 -0700 Subject: [PATCH 6/7] [Reporting] Move "common" types and constants to allow cross-plugin integration (#83198) --- x-pack/plugins/reporting/common/constants.ts | 41 +++++- x-pack/plugins/reporting/common/index.ts | 9 ++ x-pack/plugins/reporting/common/poller.ts | 11 +- x-pack/plugins/reporting/common/types.ts | 127 +++++++++++++++--- x-pack/plugins/reporting/constants.ts | 39 ------ .../buttons/report_download_button.tsx | 4 +- .../buttons/report_error_button.tsx | 4 +- .../components/buttons/report_info_button.tsx | 2 +- .../reporting/public/components/index.ts | 1 + .../public/components/job_download_button.tsx | 3 +- .../public/components/job_failure.tsx | 2 +- .../public/components/job_success.tsx | 3 +- .../components/job_warning_formulas.tsx | 3 +- .../components/job_warning_max_size.tsx | 3 +- .../public/components/report_listing.tsx | 2 +- .../components/reporting_panel_content.tsx | 7 +- x-pack/plugins/reporting/public/index.ts | 34 ++--- .../lib/job_completion_notifications.ts | 2 +- .../public/lib/reporting_api_client.ts | 12 +- .../public/lib/stream_handler.test.ts | 3 +- .../reporting/public/lib/stream_handler.ts | 14 +- .../panel_actions/get_csv_panel_action.tsx | 16 +-- x-pack/plugins/reporting/public/plugin.ts | 32 ++++- .../server/export_types/csv/create_job.ts | 2 +- .../server/export_types/csv/index.ts | 2 +- .../csv_from_savedobject/index.ts | 2 +- .../csv_from_savedobject/metadata.ts | 2 +- .../export_types/png/create_job/index.ts | 2 +- .../printable_pdf/create_job/index.ts | 2 +- .../server/lib/layouts/create_layout.ts | 5 +- .../reporting/server/lib/layouts/index.ts | 54 ++------ .../server/lib/layouts/preserve_layout.ts | 15 +-- .../server/lib/layouts/print_layout.ts | 14 +- .../reporting/server/lib/store/index.ts | 3 +- .../reporting/server/lib/store/report.ts | 51 +------ .../reporting/server/lib/tasks/index.ts | 11 +- .../generate_from_savedobject_immediate.ts | 4 +- .../create_mock_layoutinstance.ts | 5 +- x-pack/plugins/reporting/server/types.ts | 13 +- 39 files changed, 282 insertions(+), 279 deletions(-) delete mode 100644 x-pack/plugins/reporting/constants.ts diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 07a239494da239..16e40bab65a46e 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -11,13 +11,6 @@ export const BROWSER_TYPE = 'chromium'; export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu -export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; -export const API_LIST_URL = '/api/reporting/jobs'; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; -export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; - export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; @@ -57,15 +50,49 @@ export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; +export const LAYOUT_TYPES = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', +}; + +// Export Type Definitions +export const CSV_REPORT_TYPE = 'CSV'; +export const PDF_REPORT_TYPE = 'printablePdf'; +export const PNG_REPORT_TYPE = 'PNG'; + export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; +// Licenses export const LICENSE_TYPE_TRIAL = 'trial'; export const LICENSE_TYPE_BASIC = 'basic'; export const LICENSE_TYPE_STANDARD = 'standard'; export const LICENSE_TYPE_GOLD = 'gold'; export const LICENSE_TYPE_PLATINUM = 'platinum'; export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; + +// Routes +export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; + +// hacky endpoint +export const API_BASE_URL_V1 = '/api/reporting/v1'; // +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; + +// Management UI route +export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; + +// Statuses +export enum JOB_STATUSES { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', +} diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index cda8934fc8bf62..0be6ab66827746 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -4,5 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LayoutSelectorDictionary } from './types'; + export { CancellationToken } from './cancellation_token'; export { Poller } from './poller'; + +export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}); diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 2127a876f4a271..017dbac13e29b5 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -5,7 +5,16 @@ */ import _ from 'lodash'; -import { PollerOptions } from './types'; + +interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} // @TODO Maybe move to observables someday export class Poller { diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 24c126bfe0571f..abd0bee7fb6ea2 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -4,15 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportingConfigType } from '../server/config'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutParams } from '../server/lib/layouts'; -export { LayoutParams }; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportDocument, ReportSource } from '../server/lib/store/report'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { BaseParams } from '../server/types'; +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface LayoutParams { + id: string; + dimensions?: Size; + selectors?: LayoutSelectorDictionary; +} + +export interface ReportDocumentHead { + _id: string; + _index: string; + _seq_no: unknown; + _primary_term: unknown; +} + +export interface TaskRunResult { + content_type: string | null; + content: string | null; + csv_contains_formulas?: boolean; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} + +export interface ReportSource { + jobtype: string; + kibana_name: string; + kibana_id: string; + created_by: string | false; + payload: { + headers: string; // encrypted headers + browserTimezone?: string; // may use timezone from advanced settings + objectType: string; + title: string; + layout?: LayoutParams; + }; + meta: { objectType: string; layout?: string }; + browser_type: string; + max_attempts: number; + timeout: number; + + status: JobStatus; + attempts: number; + output: TaskRunResult | null; + started_at?: string; + completed_at?: string; + created_at: string; + priority?: number; + process_expiration?: string; +} + +/* + * The document created by Reporting to store in the .reporting index + */ +export interface ReportDocument extends ReportDocumentHead { + _source: ReportSource; +} + +export interface BaseParams { + browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface + layout?: LayoutParams; + objectType: string; + title: string; +} export type JobId = string; export type JobStatus = @@ -59,18 +138,28 @@ export interface ReportApiJSON { status: string; } -export interface PollerOptions { - functionToPoll: () => Promise; - pollFrequencyInMillis: number; - trailing?: boolean; - continuePollingOnError?: boolean; - pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; -} - export interface LicenseCheckResults { enableLinks: boolean; showLinks: boolean; message: string; } + +export interface JobSummary { + id: JobId; + status: JobStatus; + title: string; + jobtype: string; + maxSizeReached?: boolean; + csvContainsFormulas?: boolean; +} + +export interface JobSummarySet { + completed: JobSummary[]; + failed: JobSummary[]; +} + +type DownloadLink = string; +export type DownloadReportFn = (jobId: JobId) => DownloadLink; + +type ManagementLink = string; +export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts deleted file mode 100644 index 772c52dde4a152..00000000000000 --- a/x-pack/plugins/reporting/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = - 'xpack.reporting.jobCompletionNotifications'; - -// Routes -export const API_BASE_URL = '/api/reporting'; -export const API_LIST_URL = `${API_BASE_URL}/jobs`; -export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; -export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; - -// Statuses -export const JOB_STATUS_FAILED = 'failed'; -export const JOB_STATUS_COMPLETED = 'completed'; -export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - WARNINGS = 'completed_with_warnings', -} - -// Types -export const PDF_JOB_TYPE = 'printable_pdf'; -export const PNG_JOB_TYPE = 'PNG'; -export const CSV_JOB_TYPE = 'csv'; -export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; -export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; - -// Actions -export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index 6c13264ebcb1fc..4bd86d15949e85 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { Job as ListingJob, Props as ListingProps } from '../report_listing'; type Props = { record: ListingJob } & ListingProps; @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { + if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 4eee86cd79ce72..2864802f843f45 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,7 +7,7 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; import { Job as ListingJob } from '../report_listing'; @@ -43,7 +43,7 @@ class ReportErrorButtonUi extends Component { public render() { const { record, intl } = this.props; - if (record.status !== JobStatuses.FAILED) { + if (record.status !== JOB_STATUSES.FAILED) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 068cb7d44b0a16..0e249f156f5874 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { get } from 'lodash'; import React, { Component, Fragment } from 'react'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; import { ReportApiJSON } from '../../../common/types'; -import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; import { ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { diff --git a/x-pack/plugins/reporting/public/components/index.ts b/x-pack/plugins/reporting/public/components/index.ts index 354ef189704ad0..370e90c8d2d08f 100644 --- a/x-pack/plugins/reporting/public/components/index.ts +++ b/x-pack/plugins/reporting/public/components/index.ts @@ -9,3 +9,4 @@ export { getFailureToast } from './job_failure'; export { getWarningFormulasToast } from './job_warning_formulas'; export { getWarningMaxSizeToast } from './job_warning_max_size'; export { getGeneralErrorToast } from './general_error'; +export { ScreenCapturePanelContent } from './screen_capture_panel_content'; diff --git a/x-pack/plugins/reporting/public/components/job_download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx index 8cf3ce8644add1..7dff2cafa047b2 100644 --- a/x-pack/plugins/reporting/public/components/job_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/job_download_button.tsx @@ -7,8 +7,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { JobSummary } from '../'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; interface Props { getUrl: (jobId: JobId) => string; diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 8d8f32f692343d..e9c3a448cfe412 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary, ManagementLinkFn } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { JobSummary, ManagementLinkFn } from '../../common/types'; export const getFailureToast = ( errorText: string, diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index 05cf2c4c5784a6..f03914b2be2f2d 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 8cccc94e98dcda..338c718a060c1e 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index c350eef0e5a547..cab743e2006df0 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index a512b1305b8e03..ac6d03a407c289 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -22,9 +22,9 @@ import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; +import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 18895f9e623eb9..7f48b5d9101baa 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -10,6 +10,7 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -165,12 +166,12 @@ class ReportingPanelContentUi extends Component { private prettyPrintReportingType = () => { switch (this.props.reportType) { - case 'printablePdf': + case PDF_REPORT_TYPE: return 'PDF'; case 'csv': - return 'CSV'; + return CSV_REPORT_TYPE; case 'png': - return 'PNG'; + return PNG_REPORT_TYPE; default: return this.props.reportType; } diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 251fd14ee4d57c..f15a5ca4817571 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -5,33 +5,21 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { ReportingPublicPlugin } from './plugin'; +import { ScreenCapturePanelContent } from './components/screen_capture_panel_content'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; -import { JobId, JobStatus } from '../common/types'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ReportingPublicPlugin(initializerContext); +export interface ReportingSetup { + components: { + ScreenCapturePanel: typeof ScreenCapturePanelContent; + }; } -export { ReportingPublicPlugin as Plugin }; -export { jobCompletionNotifications }; +export type ReportingStart = ReportingSetup; -export interface JobSummary { - id: JobId; - status: JobStatus; - title: string; - jobtype: string; - maxSizeReached?: boolean; - csvContainsFormulas?: boolean; -} +export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications }; -export interface JobSummarySet { - completed: JobSummary[]; - failed: JobSummary[]; +export function plugin(initializerContext: PluginInitializerContext) { + return new ReportingPublicPlugin(initializerContext); } - -type DownloadLink = string; -export type DownloadReportFn = (jobId: JobId) => DownloadLink; - -type ManagementLink = string; -export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts index 06694361b757d5..39a7c9f84b8e5b 100644 --- a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants'; type JobId = string; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 2853caaaaa1b51..71b57d0c0124e3 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,14 +7,20 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; -import { DownloadReportFn, ManagementLinkFn } from '../'; -import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types'; import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, REPORTING_MANAGEMENT_HOME, -} from '../../constants'; +} from '../../common/constants'; +import { + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, + ReportDocument, + ReportSource, +} from '../../common/types'; import { add } from './job_completion_notifications'; export interface JobQueueEntry { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index f91517e4397f93..31d324bd77159b 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -6,8 +6,7 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary } from '../'; -import { ReportDocument } from '../../common/types'; +import { JobSummary, ReportDocument } from '../../common/types'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index d97c0a7a2d11ef..4b2305b60c413c 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,14 +8,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; -import { JobSummarySet, JobSummary } from '../'; -import { JobId, ReportDocument } from '../../common/types'; -import { - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, - JOB_STATUS_COMPLETED, - JOB_STATUS_FAILED, - JOB_STATUS_WARNINGS, -} from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants'; +import { JobId, JobSummary, JobSummarySet, ReportDocument } from '../../common/types'; import { getFailureToast, getGeneralErrorToast, @@ -107,9 +101,9 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { + if (jobStatus === JOB_STATUSES.COMPLETED || jobStatus === JOB_STATUSES.WARNINGS) { completedJobs.push(getReportStatus(job)); - } else if (jobStatus === JOB_STATUS_FAILED) { + } else if (jobStatus === JOB_STATUSES.FAILED) { failedJobs.push(getReportStatus(job)); } else { pending.push(jobId); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 1e3f7e34bebdb1..9a4832b114e40d 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -9,20 +9,18 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { - UiActionsActionDefinition as ActionDefinition, + ISearchEmbeddable, + SEARCH_EMBEDDABLE_TYPE, +} from '../../../../../src/plugins/discover/public'; +import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { IncompatibleActionError, + UiActionsActionDefinition as ActionDefinition, } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; -import { ViewMode, IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { - ISearchEmbeddable, - SEARCH_EMBEDDABLE_TYPE, -} from '../../../../../src/plugins/discover/public'; - -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; - function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable ): embeddable is ISearchEmbeddable { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 33f4fd4abf72cb..52362b4c68734f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -24,11 +24,14 @@ import { import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; -import { JobId, ReportingConfigType } from '../common/types'; -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; -import { JobSummarySet } from './'; -import { getGeneralErrorToast } from './components'; +import { JobId, JobSummarySet } from '../common/types'; +import { ReportingSetup, ReportingStart } from './'; +import { + getGeneralErrorToast, + ScreenCapturePanelContent as ScreenCapturePanel, +} from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -36,7 +39,12 @@ import { csvReportingProvider } from './share_context_menu/register_csv_reportin import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: ReportingConfigType['poll']; + poll: { + jobsRefresh: { + interval: number; + intervalErrorMultiplier: number; + }; + }; } function getStored(): JobId[] { @@ -75,8 +83,13 @@ export interface ReportingPublicPluginStartDendencies { export class ReportingPublicPlugin implements - Plugin { - private config: ClientConfigType; + Plugin< + ReportingSetup, + ReportingStart, + ReportingPublicPluginSetupDendencies, + ReportingPublicPluginStartDendencies + > { + private readonly contract: ReportingStart = { components: { ScreenCapturePanel } }; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -84,6 +97,7 @@ export class ReportingPublicPlugin private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); + private config: ClientConfigType; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -149,6 +163,8 @@ export class ReportingPublicPlugin uiSettings, }) ); + + return this.contract; } public start(core: CoreStart) { @@ -166,6 +182,8 @@ export class ReportingPublicPlugin catchError((err) => handleError(notifications, err)) ) .subscribe(); + + return this.contract; } public stop() { diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 5b98a198b7d1a6..43243d265e926d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_JOB_TYPE } from '../../../constants'; +import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index e66cfef18c6e26..f7b7ff5709fe69 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_JOB_TYPE as jobType, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index abe9fbf3e39506..2c163aeb57a644 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts index a0fd8a29fdcc45..fda360103a1153 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; export const metadata = { id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index b1fcdbe05fd67f..010b6f431db7ed 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PNG_JOB_TYPE } from '../../../../constants'; +import { PNG_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index dcd33a0fc8d530..a529cb864b6f73 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PDF_JOB_TYPE } from '../../../../constants'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index e69b8d61dec0d2..c90f67b81317e8 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LAYOUT_TYPES } from '../../../common/constants'; import { CaptureConfig } from '../../types'; -import { LayoutInstance, LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; @@ -13,7 +14,7 @@ export function createLayout( captureConfig: CaptureConfig, layoutParams?: LayoutParams ): LayoutInstance { - if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { + if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index c091339a605823..8bfe79aeb8a216 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,59 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../browsers'; import { LevelLogger } from '../'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { Layout } from './layout'; +export { + LayoutParams, + LayoutSelectorDictionary, + PageSizeParams, + PdfImageSize, + Size, +} from '../../../common/types'; export { createLayout } from './create_layout'; export { Layout } from './layout'; export { PreserveLayout } from './preserve_layout'; export { PrintLayout } from './print_layout'; -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - -export interface Size { - width: number; - height: number; -} - -export interface LayoutParams { - id: string; - dimensions?: Size; - selectors?: LayoutSelectorDictionary; -} - interface LayoutSelectors { // Fields that are not part of Layout: the instances // independently implement these fields on their own diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index faddaae64ce5dd..549e898d8a13ea 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -6,15 +6,10 @@ import path from 'path'; import { CustomPageSize } from 'pdfmake/interfaces'; -import { - getDefaultLayoutSelectors, - Layout, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - PageSizeParams, - Size, -} from './'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, PageSizeParams, Size } from '../../../common/types'; +import { Layout, LayoutInstance } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; @@ -28,7 +23,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { private readonly scaledWidth: number; constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) { - super(LayoutTypes.PRESERVE_LAYOUT); + super(LAYOUT_TYPES.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index e979cdeeb71fec..8db1fa7ff63478 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -8,16 +8,12 @@ import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - getDefaultLayoutSelectors, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - Size, -} from './'; -import { Layout } from './layout'; +import { Layout, LayoutInstance } from './'; export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { @@ -28,7 +24,7 @@ export class PrintLayout extends Layout implements LayoutInstance { private captureConfig: CaptureConfig; constructor(captureConfig: CaptureConfig) { - super(LayoutTypes.PRINT); + super(LAYOUT_TYPES.PRINT); this.captureConfig = captureConfig; } diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index a48f2661203236..17f0fb5bf03893 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Report, ReportDocument } from './report'; +export { ReportDocument } from '../../../common/types'; +export { Report } from './report'; export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index d82b90f4025ed2..2e4473ef8f2ea6 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -7,51 +7,8 @@ import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; -import { JobStatus, ReportApiJSON } from '../../../common/types'; -import { JobStatuses } from '../../../constants'; -import { LayoutParams } from '../layouts'; -import { TaskRunResult } from '../tasks'; - -interface ReportDocumentHead { - _id: string; - _index: string; - _seq_no: unknown; - _primary_term: unknown; -} - -/* - * The document created by Reporting to store in the .reporting index - */ -export interface ReportDocument extends ReportDocumentHead { - _source: ReportSource; -} - -export interface ReportSource { - jobtype: string; - kibana_name: string; - kibana_id: string; - created_by: string | false; - payload: { - headers: string; // encrypted headers - browserTimezone?: string; // may use timezone from advanced settings - objectType: string; - title: string; - layout?: LayoutParams; - }; - meta: { objectType: string; layout?: string }; - browser_type: string; - max_attempts: number; - timeout: number; - - status: JobStatus; - attempts: number; - output: TaskRunResult | null; - started_at?: string; - completed_at?: string; - created_at: string; - priority?: number; - process_expiration?: string; -} +import { JOB_STATUSES } from '../../../common/constants'; +import { ReportApiJSON, ReportDocumentHead, ReportSource } from '../../../common/types'; const puid = new Puid(); @@ -107,7 +64,7 @@ export class Report implements Partial { this.browser_type = opts.browser_type; this.priority = opts.priority; - this.status = opts.status || JobStatuses.PENDING; + this.status = opts.status || JOB_STATUSES.PENDING; this.output = opts.output || null; } @@ -175,3 +132,5 @@ export class Report implements Partial { }; } } + +export { ReportApiJSON, ReportSource }; diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index 0dd9945985bfb5..c866c81c9793c0 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReportSource, TaskRunResult } from '../../../common/types'; import { BasePayload } from '../../types'; -import { ReportSource } from '../store/report'; /* * The document created by Reporting to store as task parameters for Task @@ -22,11 +22,4 @@ export interface ReportTaskParams { meta: ReportSource['meta']; } -export interface TaskRunResult { - content_type: string | null; - content: string | null; - csv_contains_formulas?: boolean; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} +export { TaskRunResult }; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 400fbb16f54dcb..6ac5875acd34c6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; -import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { @@ -20,6 +19,9 @@ import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routi import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; +const API_BASE_URL_V1 = '/api/reporting/v1'; +const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; + export type CsvFromSavedObjectRequest = KibanaRequest< JobParamsPanelCsv, unknown, diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index c9dbbda9fd68de..12a3ac5c762c74 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; +import { LAYOUT_TYPES } from '../../common/constants'; +import { createLayout, LayoutInstance } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { - id: LayoutTypes.PRESERVE_LAYOUT, + id: LAYOUT_TYPES.PRESERVE_LAYOUT, dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index eb046a3eab0752..8cd26df032f641 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,15 +8,15 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { CancellationToken } from '../common'; +import { BaseParams } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; -import { LayoutParams } from './lib/layouts'; import { ReportTaskParams, TaskRunResult } from './lib/tasks'; /* @@ -47,12 +47,7 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface - layout?: LayoutParams; - objectType: string; - title: string; -} +export { BaseParams }; // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { From 208e86e66a7001a1f93f0dd0d937af5cc4deb9db Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 12 Nov 2020 11:05:17 -0500 Subject: [PATCH 7/7] [Ingest Manager] Lift up registry/{stream,extract} functions (#83239) ## Summary * Move stream utility functions from `server/services/epm/registry/streams.ts` to `server/services/epm/streams.ts` * They're only used in registry at the moment but aren't specific to registry * Move archive extraction functions from `server/services/epm/registry/extract.ts` to `server/services/epm/archive.ts` * The Registry isn't the only service/code which needs to extract packages. Continue consolidating archive-related code under archive vs registry --- .../server/services/epm/{registry => archive}/extract.ts | 8 ++------ x-pack/plugins/fleet/server/services/epm/archive/index.ts | 8 +++++++- .../fleet/server/services/epm/archive/validation.ts | 3 ++- .../services/epm/elasticsearch/ingest_pipeline/install.ts | 3 +-- .../plugins/fleet/server/services/epm/packages/assets.ts | 6 +++--- .../fleet/server/services/epm/registry/index.test.ts | 5 ++--- .../plugins/fleet/server/services/epm/registry/index.ts | 4 +--- .../fleet/server/services/epm/registry/requests.ts | 2 +- .../fleet/server/services/epm/{registry => }/streams.ts | 0 9 files changed, 19 insertions(+), 20 deletions(-) rename x-pack/plugins/fleet/server/services/epm/{registry => archive}/extract.ts (95%) rename x-pack/plugins/fleet/server/services/epm/{registry => }/streams.ts (100%) diff --git a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts similarity index 95% rename from x-pack/plugins/fleet/server/services/epm/registry/extract.ts rename to x-pack/plugins/fleet/server/services/epm/archive/extract.ts index b79218638ce247..6ac81a25dfc21e 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts @@ -6,12 +6,8 @@ import tar from 'tar'; import yauzl from 'yauzl'; -import { bufferToStream, streamToBuffer } from './streams'; - -export interface ArchiveEntry { - path: string; - buffer?: Buffer; -} +import { bufferToStream, streamToBuffer } from '../streams'; +import { ArchiveEntry } from './index'; export async function untarBuffer( buffer: Buffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 810740d697fcbc..6d1150b3ac8bd5 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -14,10 +14,16 @@ import { setArchiveFilelist, deleteArchiveFilelist, } from './cache'; -import { ArchiveEntry, getBufferExtractor } from '../registry/extract'; +import { getBufferExtractor } from './extract'; import { parseAndVerifyArchiveEntries } from './validation'; export * from './cache'; +export { untarBuffer, unzipBuffer, getBufferExtractor } from './extract'; + +export interface ArchiveEntry { + path: string; + buffer?: Buffer; +} export async function getArchivePackage({ archiveBuffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index d9d451544a9538..992020cb073add 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -15,7 +15,8 @@ import { RegistryVarsEntry, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; -import { ArchiveEntry, pkgToPkgKey } from '../registry'; +import { ArchiveEntry } from './index'; +import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; const MANIFEST_NAME = 'manifest.yml'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c5c9e8ac2c01ba..b6988f64843d09 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -11,8 +11,7 @@ import { ElasticsearchAssetType, InstallablePackage, } from '../../../../types'; -import { ArchiveEntry } from '../../registry'; -import { getAsset, getPathParts } from '../../archive'; +import { ArchiveEntry, getAsset, getPathParts } from '../../archive'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 50d8f2f4d2fb22..80e1cbba6484b6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { InstallablePackage } from '../../../types'; import * as Registry from '../registry'; -import { getArchiveFilelist, getAsset } from '../archive'; +import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml` @@ -51,14 +51,14 @@ export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): Promise { // TODO: Needs to be called to fill the cache but should not be required await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); - const entries: Registry.ArchiveEntry[] = assets.map((path) => { + const entries: ArchiveEntry[] = assets.map((path) => { const buffer = getAsset(path); return { path, buffer }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index 1208ffdaefe4af..aea28b5d56ab9d 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -5,9 +5,8 @@ */ import { AssetParts } from '../../../types'; -import { getPathParts } from '../archive'; -import { getBufferExtractor, splitPkgKey } from './index'; -import { untarBuffer, unzipBuffer } from './extract'; +import { getBufferExtractor, getPathParts, untarBuffer, unzipBuffer } from '../archive'; +import { splitPkgKey } from './index'; const testPaths = [ { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index c35e91bdf580bd..aef1bb75619cdf 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -24,13 +24,11 @@ import { unpackArchiveToCache, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; -import { streamToBuffer } from './streams'; +import { streamToBuffer } from '../streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; import { PackageNotFoundError, PackageCacheError } from '../../../errors'; -export { ArchiveEntry, getBufferExtractor } from './extract'; - export interface SearchParams { category?: CategoryId; experimental?: boolean; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts index 2b9c3495657903..c8d158c8afaaaa 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts @@ -6,7 +6,7 @@ import fetch, { FetchError, Response, RequestInit } from 'node-fetch'; import pRetry from 'p-retry'; -import { streamToString } from './streams'; +import { streamToString } from '../streams'; import { appContextService } from '../../app_context'; import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; import { getProxyAgent, getRegistryProxyUrl } from './proxy'; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/streams.ts b/x-pack/plugins/fleet/server/services/epm/streams.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/epm/registry/streams.ts rename to x-pack/plugins/fleet/server/services/epm/streams.ts