From 55c94c8430168ccaf6d4bbf0ecb2af0a0a89f45b Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 4 May 2020 11:16:02 -0400 Subject: [PATCH 001/143] [Ingest] Add data to Overview page (#65024) --- .../common/types/rest_spec/agent.ts | 2 +- .../hooks/use_request/agents.ts | 9 + .../overview/components/agent_section.tsx | 91 ++++++++++ .../components/configuration_section.tsx | 79 +++++++++ .../components/datastream_section.tsx | 101 +++++++++++ .../components/integration_section.tsx | 78 +++++++++ .../overview/components/overview_panel.tsx | 26 +++ .../overview/components/overview_stats.tsx | 23 +++ .../sections/overview/index.tsx | 163 +----------------- 9 files changed, 417 insertions(+), 155 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 64ed95db74f4c..7214611ca9122 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -152,7 +152,7 @@ export interface UpdateAgentRequest { export interface GetAgentStatusRequest { query: { - configId: string; + configId?: string; }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index 453bcf2bd81e7..cad1791af41be 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -62,6 +62,15 @@ export function sendGetAgentStatus( }); } +export function useGetAgentStatus(query: GetAgentStatusRequest['query'], options?: RequestOptions) { + return useRequest({ + method: 'get', + path: agentRouteService.getStatusPath(), + query, + ...options, + }); +} + export function sendPutAgentReassign( agentId: string, body: PutAgentReassignRequest['body'], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx new file mode 100644 index 0000000000000..0f6d3c5b55ce6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -0,0 +1,91 @@ +/* + * 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 { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetAgentStatus } from '../../../hooks'; +import { FLEET_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; + +export const OverviewAgentSection = () => { + const agentStatusRequest = useGetAgentStatus({}); + + return ( + + +
+ +

+ +

+
+ + + +
+ + {agentStatusRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + )} + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx new file mode 100644 index 0000000000000..b74cac9a62176 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetDatasources } from '../../../hooks'; +import { AgentConfig } from '../../../types'; +import { AGENT_CONFIG_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; + +export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[] }> = ({ + agentConfigs, +}) => { + const datasourcesRequest = useGetDatasources({ + page: 1, + perPage: 10000, + }); + + return ( + + +
+ +

+ +

+
+ + + +
+ + {datasourcesRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + )} + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx new file mode 100644 index 0000000000000..7d1f0598a2767 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetDataStreams, useStartDeps } from '../../../hooks'; +import { Loading } from '../../fleet/components'; +import { DATA_STREAM_PATH } from '../../../constants'; + +export const OverviewDatastreamSection: React.FC = () => { + const datastreamRequest = useGetDataStreams(); + const { + data: { fieldFormats }, + } = useStartDeps(); + + const total = datastreamRequest.data?.data_streams?.length ?? 0; + let sizeBytes = 0; + const namespaces = new Set(); + if (datastreamRequest.data) { + datastreamRequest.data.data_streams.forEach(val => { + namespaces.add(val.namespace); + sizeBytes += val.size_in_bytes; + }); + } + + let size: string; + try { + const formatter = fieldFormats.getInstance('bytes'); + size = formatter.convert(sizeBytes); + } catch (e) { + size = `${sizeBytes}b`; + } + + return ( + + +
+ +

+ +

+
+ + + +
+ + {datastreamRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + + + {size} + + )} + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx new file mode 100644 index 0000000000000..f4c122af88371 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetPackages } from '../../../hooks'; +import { EPM_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; +import { InstallationStatus } from '../../../types'; + +export const OverviewIntegrationSection: React.FC = () => { + const packagesRequest = useGetPackages(); + + const total = packagesRequest.data?.response?.length ?? 0; + const installed = + packagesRequest.data?.response?.filter(p => p.status === InstallationStatus.installed) + ?.length ?? 0; + return ( + + +
+ +

+ +

+
+ + + +
+ + {packagesRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + )} + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx new file mode 100644 index 0000000000000..41d7a7a5f0bc3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import { EuiPanel } from '@elastic/eui'; + +export const OverviewPanel = styled(EuiPanel).attrs(props => ({ + paddingSize: 'm', +}))` + header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; + margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} + ${props => props.theme.eui.paddingSizes.m}; + padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; + } + + h2 { + padding: ${props => props.theme.eui.paddingSizes.xs} 0; + } +`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx new file mode 100644 index 0000000000000..04de22c34fe6f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import { EuiDescriptionList } from '@elastic/eui'; + +export const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ + compressed: true, + textStyle: 'reverse', + type: 'column', +}))` + & > * { + margin-top: ${props => props.theme.eui.paddingSizes.s} !important; + + &:first-child, + &:nth-child(2) { + margin-top: 0 !important; + } + } +`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 70d8e7d6882f8..3cd778fb4f016 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -7,14 +7,8 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { EuiButton, - EuiButtonEmpty, EuiBetaBadge, - EuiPanel, EuiText, - EuiTitle, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, EuiFlexGrid, EuiFlexGroup, EuiFlexItem, @@ -22,42 +16,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; -import { useLink, useGetAgentConfigs } from '../../hooks'; +import { useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; - -const OverviewPanel = styled(EuiPanel).attrs(props => ({ - paddingSize: 'm', -}))` - header { - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; - margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} - ${props => props.theme.eui.paddingSizes.m}; - padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; - } - - h2 { - padding: ${props => props.theme.eui.paddingSizes.xs} 0; - } -`; - -const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ - compressed: true, - textStyle: 'reverse', - type: 'column', -}))` - & > * { - margin-top: ${props => props.theme.eui.paddingSizes.s} !important; - - &:first-child, - &:nth-child(2) { - margin-top: 0 !important; - } - } -`; +import { OverviewAgentSection } from './components/agent_section'; +import { OverviewConfigurationSection } from './components/configuration_section'; +import { OverviewIntegrationSection } from './components/integration_section'; +import { OverviewDatastreamSection } from './components/datastream_section'; const AlphaBadge = styled(EuiBetaBadge)` vertical-align: top; @@ -135,121 +99,12 @@ export const IngestManagerOverview: React.FunctionComponent = () => { )} - - -
- -

- -

-
- - - -
- - Total available - 999 - Installed - 1 - Updated available - 0 - -
-
+ + - - -
- -

- -

-
- - - -
- - Total configs - 1 - Data sources - 1 - -
-
+ - - -
- -

- -

-
- - - -
- - Total agents - 0 - Active - 0 - Offline - 0 - Error - 0 - -
-
- - - -
- -

- -

-
- - - -
- - Data streams - 0 - Name spaces - 0 - Total size - 0 MB - -
-
+
); From 3ba268d8a59a02c2d9aec123f2a701b2ba3e3f83 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 4 May 2020 10:22:40 -0500 Subject: [PATCH 002/143] [DOCS] Reformats settings tables (#64844) * Formats settings into tables * Formatting * Formatting --- docs/settings/alert-action-settings.asciidoc | 39 +- docs/settings/apm-settings.asciidoc | 39 +- docs/settings/dev-settings.asciidoc | 16 +- .../general-infra-logs-ui-settings.asciidoc | 31 +- docs/settings/graph-settings.asciidoc | 9 +- docs/settings/i18n-settings.asciidoc | 11 +- docs/settings/ml-settings.asciidoc | 27 +- docs/settings/monitoring-settings.asciidoc | 171 ++-- docs/settings/reporting-settings.asciidoc | 223 ++--- docs/settings/security-settings.asciidoc | 122 +-- docs/settings/spaces-settings.asciidoc | 20 +- docs/settings/telemetry-settings.asciidoc | 48 +- docs/setup/settings.asciidoc | 764 +++++++++++------- 13 files changed, 924 insertions(+), 596 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d4dbe9407b7a9..547b4fdedcec6 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -5,7 +5,7 @@ Alerting and action settings ++++ -Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them: +Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them: . <>. . <>. @@ -18,27 +18,36 @@ You can configure the following settings in the `kibana.yml` file. [[general-alert-action-settings]] ==== General settings -`xpack.encryptedSavedObjects.encryptionKey`:: +[cols="2*<"] +|=== -A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. -+ -If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. -+ -Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. +| `xpack.encryptedSavedObjects.encryptionKey` + | A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. + + + + If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. + + + + Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. + +|=== [float] [[action-settings]] ==== Action settings -`xpack.actions.whitelistedHosts`:: -A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. -+ -Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. +[cols="2*<"] +|=== + +| `xpack.actions.whitelistedHosts` + | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. + + + + Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. + +| `xpack.actions.enabledActionTypes` + | A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. + + + + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. -`xpack.actions.enabledActionTypes`:: -A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. -+ -Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. +|=== [float] [[alert-settings]] diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index fd53c3aeb3605..8844fcd03ae9a 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -38,27 +38,42 @@ If you'd like to change any of the default values, copy and paste the relevant settings into your `kibana.yml` configuration file. Changing these settings may disable features of the APM App. -xpack.apm.enabled:: Set to `false` to disable the APM app. Defaults to `true`. +[cols="2*<"] +|=== +| `xpack.apm.enabled` + | Set to `false` to disable the APM app. Defaults to `true`. -xpack.apm.ui.enabled:: Set to `false` to hide the APM app from the menu. Defaults to `true`. +| `xpack.apm.ui.enabled` + | Set to `false` to hide the APM app from the menu. Defaults to `true`. -xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in the APM app. Defaults to `100`. +| `xpack.apm.ui.transactionGroupBucketSize` + | Number of top transaction groups displayed in the APM app. Defaults to `100`. -xpack.apm.ui.maxTraceItems:: Maximum number of child items displayed when viewing trace details. Defaults to `1000`. +| `xpack.apm.ui.maxTraceItems` + | Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -apm_oss.indexPattern:: The index pattern used for integrations with Machine Learning and Query Bar. -It must match all apm indices. Defaults to `apm-*`. +| `apm_oss.indexPattern` + | The index pattern used for integrations with Machine Learning and Query Bar. + It must match all apm indices. Defaults to `apm-*`. -apm_oss.errorIndices:: Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. +| `apm_oss.errorIndices` + | Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. -apm_oss.onboardingIndices:: Matcher for all onboarding indices. Defaults to `apm-*`. +| `apm_oss.onboardingIndices` + | Matcher for all onboarding indices. Defaults to `apm-*`. -apm_oss.spanIndices:: Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. +| `apm_oss.spanIndices` + | Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. -apm_oss.transactionIndices:: Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. +| `apm_oss.transactionIndices` + | Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. -apm_oss.metricsIndices:: Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. +| `apm_oss.metricsIndices` + | Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. -apm_oss.sourcemapIndices:: Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. +| `apm_oss.sourcemapIndices` + | Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. + +|=== // end::general-apm-settings[] diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index 436f169b82ca3..c43b96a8668e0 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -12,12 +12,20 @@ They are enabled by default. [[grok-settings]] ==== Grok Debugger settings -`xpack.grokdebugger.enabled`:: -Set to `true` (default) to enable the <>. +[cols="2*<"] +|=== +| `xpack.grokdebugger.enabled` + | Set to `true` to enable the <>. Defaults to `true`. + +|=== [float] [[profiler-settings]] ==== {searchprofiler} Settings -`xpack.searchprofiler.enabled`:: -Set to `true` (default) to enable the <>. +[cols="2*<"] +|=== +| `xpack.searchprofiler.enabled` + | Set to `true` to enable the <>. Defaults to `true`. + +|=== diff --git a/docs/settings/general-infra-logs-ui-settings.asciidoc b/docs/settings/general-infra-logs-ui-settings.asciidoc index 7b32372a1f59a..2a9d4df1ff43c 100644 --- a/docs/settings/general-infra-logs-ui-settings.asciidoc +++ b/docs/settings/general-infra-logs-ui-settings.asciidoc @@ -1,17 +1,30 @@ -`xpack.infra.enabled`:: Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. +[cols="2*<"] +|=== +| `xpack.infra.enabled` + | Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. -`xpack.infra.sources.default.logAlias`:: Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +| `xpack.infra.sources.default.logAlias` + | Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -`xpack.infra.sources.default.metricAlias`:: Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +| `xpack.infra.sources.default.metricAlias` + | Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -`xpack.infra.sources.default.fields.timestamp`:: Timestamp used to sort log entries. Defaults to `@timestamp`. +| `xpack.infra.sources.default.fields.timestamp` + | Timestamp used to sort log entries. Defaults to `@timestamp`. -`xpack.infra.sources.default.fields.message`:: Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. +| `xpack.infra.sources.default.fields.message` + | Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. -`xpack.infra.sources.default.fields.tiebreaker`:: Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. +| `xpack.infra.sources.default.fields.tiebreaker` + | Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. -`xpack.infra.sources.default.fields.host`:: Field used to identify hosts. Defaults to `host.name`. +| `xpack.infra.sources.default.fields.host` + | Field used to identify hosts. Defaults to `host.name`. -`xpack.infra.sources.default.fields.container`:: Field used to identify Docker containers. Defaults to `container.id`. +| `xpack.infra.sources.default.fields.container` + | Field used to identify Docker containers. Defaults to `container.id`. -`xpack.infra.sources.default.fields.pod`:: Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. +| `xpack.infra.sources.default.fields.pod` + | Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. + +|=== diff --git a/docs/settings/graph-settings.asciidoc b/docs/settings/graph-settings.asciidoc index 7e597362b1cfc..8ccff21a26f74 100644 --- a/docs/settings/graph-settings.asciidoc +++ b/docs/settings/graph-settings.asciidoc @@ -10,5 +10,10 @@ You do not need to configure any settings to use the {graph-features}. [float] [[general-graph-settings]] ==== General graph settings -`xpack.graph.enabled`:: -Set to `false` to disable the {graph-features}. + +[cols="2*<"] +|=== +| `xpack.graph.enabled` + | Set to `false` to disable the {graph-features}. + +|=== diff --git a/docs/settings/i18n-settings.asciidoc b/docs/settings/i18n-settings.asciidoc index 4fe466bcb4580..6d92e74f17cb2 100644 --- a/docs/settings/i18n-settings.asciidoc +++ b/docs/settings/i18n-settings.asciidoc @@ -9,10 +9,7 @@ You do not need to configure any settings to run Kibana in English. ==== General i18n Settings `i18n.locale`:: -Kibana currently supports the following locales: -+ -- English - `en` (default) -- Chinese - `zh-CN` -- Japanese - `ja-JP` - - + {kib} supports the following locales: + * English - `en` (default) + * Chinese - `zh-CN` + * Japanese - `ja-JP` diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 36578c909f513..24e38e73bca9b 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -11,12 +11,25 @@ enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings -`xpack.ml.enabled`:: -Set to `true` (default) to enable {kib} {ml-features}. + -+ -If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} -instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, -you can still use the {ml} APIs. To disable {ml} entirely, see the -{ref}/ml-settings.html[{es} {ml} settings]. +[cols="2*<"] +|=== +| `xpack.ml.enabled` + | Set to `true` (default) to enable {kib} {ml-features}. + + + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} + instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, + you can still use the {ml} APIs. To disable {ml} entirely, see the + {ref}/ml-settings.html[{es} {ml} settings]. +|=== +[[data-visualizer-settings]] +==== {data-viz} settings + +[cols="2*<"] +|=== +| `xpack.ml.file_data_visualizer.max_file_size` + | Sets the file size limit when importing data in the {data-viz}. The default + value is `100MB`. The highest supported value for this setting is `1GB`. + +|=== diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 6645f49029a51..f180f2c3ecc97 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -29,45 +29,49 @@ For more information, see [[monitoring-general-settings]] ==== General monitoring settings -`monitoring.enabled`:: -Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the -`monitoring.ui.enabled` setting, when this setting is `false`, the -monitoring back-end does not run and {kib} stats are not sent to the monitoring -cluster. - -`monitoring.ui.elasticsearch.hosts`:: -Specifies the location of the {es} cluster where your monitoring data is stored. -By default, this is the same as `elasticsearch.hosts`. This setting enables -you to use a single {kib} instance to search and visualize data in your -production cluster as well as monitor data sent to a dedicated monitoring -cluster. - -`monitoring.ui.elasticsearch.username`:: -Specifies the username used by {kib} monitoring to establish a persistent connection -in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} -monitoring cluster. - -Every other request performed by the Stack Monitoring UI to the monitoring {es} -cluster uses the authenticated user's credentials, which must be the same on -both the {es} monitoring cluster and the {es} production cluster. - -If not set, {kib} uses the value of the `elasticsearch.username` setting. - -`monitoring.ui.elasticsearch.password`:: -Specifies the password used by {kib} monitoring to establish a persistent connection -in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} -monitoring cluster. - -Every other request performed by the Stack Monitoring UI to the monitoring {es} -cluster uses the authenticated user's credentials, which must be the same on -both the {es} monitoring cluster and the {es} production cluster. - -If not set, {kib} uses the value of the `elasticsearch.password` setting. - -`monitoring.ui.elasticsearch.pingTimeout`:: -Specifies the time in milliseconds to wait for {es} to respond to internal -health checks. By default, it matches the `elasticsearch.pingTimeout` setting, -which has a default value of `30000`. +[cols="2*<"] +|=== +| `monitoring.enabled` + | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the + `monitoring.ui.enabled` setting, when this setting is `false`, the + monitoring back-end does not run and {kib} stats are not sent to the monitoring + cluster. + +| `monitoring.ui.elasticsearch.hosts` + | Specifies the location of the {es} cluster where your monitoring data is stored. + By default, this is the same as `elasticsearch.hosts`. This setting enables + you to use a single {kib} instance to search and visualize data in your + production cluster as well as monitor data sent to a dedicated monitoring + cluster. + +| `monitoring.ui.elasticsearch.username` + | Specifies the username used by {kib} monitoring to establish a persistent connection + in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} + monitoring cluster. + + + + Every other request performed by the Stack Monitoring UI to the monitoring {es} + cluster uses the authenticated user's credentials, which must be the same on + both the {es} monitoring cluster and the {es} production cluster. + + + + If not set, {kib} uses the value of the `elasticsearch.username` setting. + +| `monitoring.ui.elasticsearch.password` + | Specifies the password used by {kib} monitoring to establish a persistent connection + in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} + monitoring cluster. + + + + Every other request performed by the Stack Monitoring UI to the monitoring {es} + cluster uses the authenticated user's credentials, which must be the same on + both the {es} monitoring cluster and the {es} production cluster. + + + + If not set, {kib} uses the value of the `elasticsearch.password` setting. + +| `monitoring.ui.elasticsearch.pingTimeout` + | Specifies the time in milliseconds to wait for {es} to respond to internal + health checks. By default, it matches the `elasticsearch.pingTimeout` setting, + which has a default value of `30000`. + +|=== [float] [[monitoring-collection-settings]] @@ -75,15 +79,18 @@ which has a default value of `30000`. These settings control how data is collected from {kib}. -`monitoring.kibana.collection.enabled`:: -Set to `true` (default) to enable data collection from the {kib} NodeJS server -for {kib} Dashboards to be featured in the Monitoring. +[cols="2*<"] +|=== +| `monitoring.kibana.collection.enabled` + | Set to `true` (default) to enable data collection from the {kib} NodeJS server + for {kib} Dashboards to be featured in the Monitoring. -`monitoring.kibana.collection.interval`:: -Specifies the number of milliseconds to wait in between data sampling on the -{kib} NodeJS server for the metrics that are displayed in the {kib} dashboards. -Defaults to `10000` (10 seconds). +| `monitoring.kibana.collection.interval` + | Specifies the number of milliseconds to wait in between data sampling on the + {kib} NodeJS server for the metrics that are displayed in the {kib} dashboards. + Defaults to `10000` (10 seconds). +|=== [float] [[monitoring-ui-settings]] @@ -94,27 +101,31 @@ However, the defaults work best in most circumstances. For more information about configuring {kib}, see {kibana-ref}/settings.html[Setting Kibana Server Properties]. -`monitoring.ui.elasticsearch.logFetchCount`:: -Specifies the number of log entries to display in the Monitoring UI. Defaults to -`10`. The maximum value is `50`. +[cols="2*<"] +|=== +| `monitoring.ui.elasticsearch.logFetchCount` + | Specifies the number of log entries to display in the Monitoring UI. Defaults to + `10`. The maximum value is `50`. -`monitoring.ui.max_bucket_size`:: -Specifies the number of term buckets to return out of the overall terms list when -performing terms aggregations to retrieve index and node metrics. For more -information about the `size` parameter, see -{ref}/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-size[Terms Aggregation]. -Defaults to `10000`. +| `monitoring.ui.max_bucket_size` + | Specifies the number of term buckets to return out of the overall terms list when + performing terms aggregations to retrieve index and node metrics. For more + information about the `size` parameter, see + {ref}/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-size[Terms Aggregation]. + Defaults to `10000`. -`monitoring.ui.min_interval_seconds`:: -Specifies the minimum number of seconds that a time bucket in a chart can -represent. Defaults to 10. If you modify the -`monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same -value in this setting. +| `monitoring.ui.min_interval_seconds` + | Specifies the minimum number of seconds that a time bucket in a chart can + represent. Defaults to 10. If you modify the + `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same + value in this setting. -`monitoring.ui.enabled`:: -Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end -continues to run as an agent for sending {kib} stats to the monitoring -cluster. Defaults to `true`. +| `monitoring.ui.enabled` + | Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end + continues to run as an agent for sending {kib} stats to the monitoring + cluster. Defaults to `true`. + +|=== [float] [[monitoring-ui-cgroup-settings]] @@ -125,18 +136,20 @@ better decisions about your container performance, rather than guessing based on the overall machine performance. If you are not running your applications in a container, then Cgroup statistics are not useful. -`monitoring.ui.container.elasticsearch.enabled`:: - -For {es} clusters that are running in containers, this setting changes the -*Node Listing* to display the CPU utilization based on the reported Cgroup -statistics. It also adds the calculated Cgroup CPU utilization to the -*Node Overview* page instead of the overall operating system's CPU -utilization. Defaults to `false`. - -`monitoring.ui.container.logstash.enabled`:: - -For {ls} nodes that are running in containers, this setting -changes the {ls} *Node Listing* to display the CPU utilization -based on the reported Cgroup statistics. It also adds the -calculated Cgroup CPU utilization to the {ls} node detail -pages instead of the overall operating system’s CPU utilization. Defaults to `false`. +[cols="2*<"] +|=== +| `monitoring.ui.container.elasticsearch.enabled` + | For {es} clusters that are running in containers, this setting changes the + *Node Listing* to display the CPU utilization based on the reported Cgroup + statistics. It also adds the calculated Cgroup CPU utilization to the + *Node Overview* page instead of the overall operating system's CPU + utilization. Defaults to `false`. + +| `monitoring.ui.container.logstash.enabled` + | For {ls} nodes that are running in containers, this setting + changes the {ls} *Node Listing* to display the CPU utilization + based on the reported Cgroup statistics. It also adds the + calculated Cgroup CPU utilization to the {ls} node detail + pages instead of the overall operating system’s CPU utilization. Defaults to `false`. + +|=== diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9a45fb9ab1d0c..7c50dbf542d0d 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -14,45 +14,54 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: [float] [[general-reporting-settings]] ==== General reporting settings -[[xpack-enable-reporting]]`xpack.reporting.enabled`:: -Set to `false` to disable the {report-features}. -`xpack.reporting.encryptionKey`:: -Set to any text string. By default, Kibana will generate a random key when it -starts, which will cause pending reports to fail after restart. Configure this -setting to preserve the same key across multiple restarts and multiple instances of Kibana. +[cols="2*<"] +|=== +| [[xpack-enable-reporting]]`xpack.reporting.enabled` + | Set to `false` to disable the {report-features}. + +| `xpack.reporting.encryptionKey` + | Set to any text string. By default, {kib} will generate a random key when it + starts, which will cause pending reports to fail after restart. Configure this + setting to preserve the same key across multiple restarts and multiple instances of {kib}. + +|=== [float] [[reporting-kibana-server-settings]] -==== Kibana server settings +==== {kib} server settings -Reporting opens the {kib} web interface in a server process to generate -screenshots of {kib} visualizations. In most cases, the default settings -will work and you don't need to configure Reporting to communicate with {kib}. +Reporting opens the {kib} web interface in a server process to generate +screenshots of {kib} visualizations. In most cases, the default settings +will work and you don't need to configure Reporting to communicate with {kib}. However, if your client connections must go through a reverse-proxy -to access {kib}, Reporting configuration must have the proxy port, protocol, +to access {kib}, Reporting configuration must have the proxy port, protocol, and hostname set in the `xpack.reporting.kibanaServer.*` settings. -[NOTE] +[NOTE] ==== -If a reverse-proxy carries encrypted traffic from end-user -clients back to a {kib} server, the proxy port, protocol, and hostname -in Reporting settings must be valid for the encryption that the Reporting -browser will receive. Encrypted communications will fail if there are +If a reverse-proxy carries encrypted traffic from end-user +clients back to a {kib} server, the proxy port, protocol, and hostname +in Reporting settings must be valid for the encryption that the Reporting +browser will receive. Encrypted communications will fail if there are mismatches in the host information between the request and the certificate on the server. Configuring the `xpack.reporting.kibanaServer` settings to point to a -proxy host requires that the Kibana server has network access to the proxy. +proxy host requires that the {kib} server has network access to the proxy. ==== -`xpack.reporting.kibanaServer.port`:: -The port for accessing Kibana, if different from the `server.port` value. +[cols="2*<"] +|=== +| `xpack.reporting.kibanaServer.port` + | The port for accessing {kib}, if different from the `server.port` value. + +| `xpack.reporting.kibanaServer.protocol` + | The protocol for accessing {kib}, typically `http` or `https`. -`xpack.reporting.kibanaServer.protocol`:: -The protocol for accessing Kibana, typically `http` or `https`. +| `xpack.reporting.kibanaServer.hostname` + | The hostname for accessing {kib}, if different from the `server.host` value. -`xpack.reporting.kibanaServer.hostname`:: -The hostname for accessing {kib}, if different from the `server.host` value. +|=== [NOTE] ============ @@ -68,55 +77,67 @@ because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0 ==== Background job settings Reporting generates reports in the background and jobs are coordinated using documents -in Elasticsearch. Depending on how often you generate reports and the overall number of +in {es}. Depending on how often you generate reports and the overall number of reports, you might need to change the following settings. -`xpack.reporting.queue.indexInterval`:: -How often the index that stores reporting jobs rolls over to a new index. -Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. +[cols="2*<"] +|=== +| `xpack.reporting.queue.indexInterval` + | How often the index that stores reporting jobs rolls over to a new index. + Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. -`xpack.reporting.queue.pollEnabled`:: -Set to `true` (default) to enable the Kibana instance to to poll the index for -pending jobs and claim them for execution. Setting this to `false` allows the -Kibana instance to only add new jobs to the reporting queue, list jobs, and -provide the downloads to completed report through the UI. +| `xpack.reporting.queue.pollEnabled` + | Set to `true` (default) to enable the {kib} instance to to poll the index for + pending jobs and claim them for execution. Setting this to `false` allows the + {kib} instance to only add new jobs to the reporting queue, list jobs, and + provide the downloads to completed report through the UI. + +|=== [NOTE] ============ -Running multiple instances of Kibana in a cluster for load balancing of +Running multiple instances of {kib} in a cluster for load balancing of reporting requires identical values for `xpack.reporting.encryptionKey` and, if security is enabled, `xpack.security.encryptionKey`. ============ -`xpack.reporting.queue.pollInterval`:: -Specifies the number of milliseconds that the reporting poller waits between polling the -index for any pending Reporting jobs. Defaults to `3000` (3 seconds). +[cols="2*<"] +|=== +| `xpack.reporting.queue.pollInterval` + | Specifies the number of milliseconds that the reporting poller waits between polling the + index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + +| [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` + | How long each worker has to produce a report. If your machine is slow or under + heavy load, you might need to increase this timeout. Specified in milliseconds. + If a Reporting job execution time goes over this time limit, the job will be + marked as a failure and there will not be a download available. + Defaults to `120000` (two minutes). -[[xpack-reporting-q-timeout]]`xpack.reporting.queue.timeout`:: -How long each worker has to produce a report. If your machine is slow or under -heavy load, you might need to increase this timeout. Specified in milliseconds. -If a Reporting job execution time goes over this time limit, the job will be -marked as a failure and there will not be a download available. -Defaults to `120000` (two minutes). +|=== [float] [[reporting-capture-settings]] ==== Capture settings -Reporting works by capturing screenshots from Kibana. The following settings +Reporting works by capturing screenshots from {kib}. The following settings control the capturing process. -`xpack.reporting.capture.timeouts.openUrl`:: -How long to allow the Reporting browser to wait for the initial data of the -Kibana page to load. Defaults to `30000` (30 seconds). +[cols="2*<"] +|=== +| `xpack.reporting.capture.timeouts.openUrl` + | How long to allow the Reporting browser to wait for the initial data of the + {kib} page to load. Defaults to `30000` (30 seconds). + +| `xpack.reporting.capture.timeouts.waitForElements` + | How long to allow the Reporting browser to wait for the visualization panels to + load on the {kib} page. Defaults to `30000` (30 seconds). -`xpack.reporting.capture.timeouts.waitForElements`:: -How long to allow the Reporting browser to wait for the visualization panels to -load on the Kibana page. Defaults to `30000` (30 seconds). +| `xpack.reporting.capture.timeouts.renderComplete` + | How long to allow the Reporting browser to wait for each visualization to + signal that it is done renderings. Defaults to `30000` (30 seconds). -`xpack.reporting.capture.timeouts.renderComplete`:: -How long to allow the Reporting brwoser to wait for each visualization to -signal that it is done renderings. Defaults to `30000` (30 seconds). +|=== [NOTE] ============ @@ -126,20 +147,24 @@ capturing the page with a screenshot. As a result, a download will be available, but there will likely be errors in the visualizations in the report. ============ -`xpack.reporting.capture.maxAttempts`:: -If capturing a report fails for any reason, Kibana will re-attempt othe reporting -job, as many times as this setting. Defaults to `3`. +[cols="2*<"] +|=== +| `xpack.reporting.capture.maxAttempts` + | If capturing a report fails for any reason, {kib} will re-attempt other reporting + job, as many times as this setting. Defaults to `3`. -`xpack.reporting.capture.loadDelay`:: -When visualizations are not evented, this is the amount of time before -taking a screenshot. All visualizations that ship with Kibana are evented, so this -setting should not have much effect. If you are seeing empty images instead of -visualizations, try increasing this value. -Defaults to `3000` (3 seconds). +| `xpack.reporting.capture.loadDelay` + | When visualizations are not evented, this is the amount of time before + taking a screenshot. All visualizations that ship with {kib} are evented, so this + setting should not have much effect. If you are seeing empty images instead of + visualizations, try increasing this value. + Defaults to `3000` (3 seconds). -[[xpack-reporting-browser]]`xpack.reporting.capture.browser.type`:: -Specifies the browser to use to capture screenshots. This setting exists for -backward compatibility. The only valid option is `chromium`. +| [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` + | Specifies the browser to use to capture screenshots. This setting exists for + backward compatibility. The only valid option is `chromium`. + +|=== [float] [[reporting-chromium-settings]] @@ -147,47 +172,59 @@ backward compatibility. The only valid option is `chromium`. When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you can also specify the following settings. -`xpack.reporting.capture.browser.chromium.disableSandbox`:: -Elastic recommends that you research the feasibility of enabling unprivileged user namespaces. -See Chromium Sandbox for additional information. Defaults to false for all operating systems except Debian, -Red Hat Linux, and CentOS which use true +[cols="2*<"] +|=== +| `xpack.reporting.capture.browser.chromium.disableSandbox` + | It is recommended that you research the feasibility of enabling unprivileged user namespaces. + See Chromium Sandbox for additional information. Defaults to false for all operating systems except Debian, + Red Hat Linux, and CentOS which use true. -`xpack.reporting.capture.browser.chromium.proxy.enabled`:: -Enables the proxy for Chromium to use. When set to `true`, you must also specify the -`xpack.reporting.capture.browser.chromium.proxy.server` setting. -Defaults to `false` +| `xpack.reporting.capture.browser.chromium.proxy.enabled` + | Enables the proxy for Chromium to use. When set to `true`, you must also specify the + `xpack.reporting.capture.browser.chromium.proxy.server` setting. + Defaults to `false`. -`xpack.reporting.capture.browser.chromium.proxy.server`:: -The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. +| `xpack.reporting.capture.browser.chromium.proxy.server` + | The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. -`xpack.reporting.capture.browser.chromium.proxy.bypass`:: -An array of hosts that should not go through the proxy server and should use a direct connection instead. -Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601" +| `xpack.reporting.capture.browser.chromium.proxy.bypass` + | An array of hosts that should not go through the proxy server and should use a direct connection instead. + Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". +|=== [float] [[reporting-csv-settings]] ==== CSV settings -[[xpack-reporting-csv]]`xpack.reporting.csv.maxSizeBytes`:: -The maximum size of a CSV file before being truncated. This setting exists to prevent -large exports from causing performance and storage issues. -Defaults to `10485760` (10mB) + +[cols="2*<"] +|=== +| [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` + | The maximum size of a CSV file before being truncated. This setting exists to prevent + large exports from causing performance and storage issues. + Defaults to `10485760` (10mB). + +|=== [float] [[reporting-advanced-settings]] ==== Advanced settings -`xpack.reporting.index`:: -Reporting uses a weekly index in Elasticsearch to store the reporting job and -the report content. The index is automatically created if it does not already -exist. Configure this to a unique value, beginning with `.reporting-`, for every -Kibana instance that has a unique `kibana.index` setting. Defaults to `.reporting` +[cols="2*<"] +|=== +| `xpack.reporting.index` + | Reporting uses a weekly index in {es} to store the reporting job and + the report content. The index is automatically created if it does not already + exist. Configure this to a unique value, beginning with `.reporting-`, for every + {kib} instance that has a unique `kibana.index` setting. Defaults to `.reporting`. + +| `xpack.reporting.roles.allow` + | Specifies the roles in addition to superusers that can use reporting. + Defaults to `[ "reporting_user" ]`. + -`xpack.reporting.roles.allow`:: -Specifies the roles in addition to superusers that can use reporting. -Defaults to `[ "reporting_user" ]` -+ --- -NOTE: Each user has access to only their own reports. +|=== --- +[NOTE] +============ +Each user has access to only their own reports. +============ diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 16d68a7759f77..8f6905d643139 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -12,55 +12,83 @@ You do not need to configure any additional settings to use the [[general-security-settings]] ==== General security settings -`xpack.security.enabled`:: -By default, {kib} automatically detects whether to enable the -{security-features} based on the license and whether {es} {security-features} -are enabled. -+ -Do not set this to `false`; it disables the login form, user and role management -screens, and authorization using <>. To disable -{security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. - -`xpack.security.audit.enabled`:: -Set to `true` to enable audit logging for security events. By default, it is set -to `false`. For more details see <>. +[cols="2*<"] +|=== +| `xpack.security.enabled` + | By default, {kib} automatically detects whether to enable the + {security-features} based on the license and whether {es} {security-features} + are enabled. + + + + Do not set this to `false`; it disables the login form, user and role management + screens, and authorization using <>. To disable + {security-features} entirely, see + {ref}/security-settings.html[{es} security settings]. + +| `xpack.security.audit.enabled` + | Set to `true` to enable audit logging for security events. By default, it is set + to `false`. For more details see <>. + +|=== [float] [[security-ui-settings]] ==== User interface security settings -You can configure the following settings in the `kibana.yml` file: - -`xpack.security.cookieName`:: -Sets the name of the cookie used for the session. The default value is `"sid"`. - -`xpack.security.encryptionKey`:: -An arbitrary string of 32 characters or more that is used to encrypt credentials -in a cookie. It is crucial that this key is not exposed to users of {kib}. By -default, a value is automatically generated in memory. If you use that default -behavior, all sessions are invalidated when {kib} restarts. -In addition, high-availability deployments of {kib} will behave unexpectedly -if this setting isn't the same for all instances of {kib}. - -`xpack.security.secureCookies`:: -Sets the `secure` flag of the session cookie. The default value is `false`. It -is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set -this to `true` if SSL is configured outside of {kib} (for example, you are -routing requests through a load balancer or proxy). - -`xpack.security.session.idleTimeout`:: -Sets the session duration. The format is a string of `[ms|s|m|h|d|w|M|Y]` -(e.g. '70ms', '5s', '3d', '1Y'). By default, sessions stay active until the -browser is closed. When this is set to an explicit idle timeout, closing the -browser still requires the user to log back in to {kib}. - -`xpack.security.session.lifespan`:: -Sets the maximum duration, also known as "absolute timeout". The format is a -string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). By default, -a session can be renewed indefinitely. When this value is set, a session will end -once its lifespan is exceeded, even if the user is not idle. NOTE: if `idleTimeout` -is not set, this setting will still cause sessions to expire. - -`xpack.security.loginAssistanceMessage`:: -Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. +You can configure the following settings in the `kibana.yml` file. + +[cols="2*<"] +|=== +| `xpack.security.cookieName` + | Sets the name of the cookie used for the session. The default value is `"sid"`. + +| `xpack.security.encryptionKey` + | An arbitrary string of 32 characters or more that is used to encrypt credentials + in a cookie. It is crucial that this key is not exposed to users of {kib}. By + default, a value is automatically generated in memory. If you use that default + behavior, all sessions are invalidated when {kib} restarts. + In addition, high-availability deployments of {kib} will behave unexpectedly + if this setting isn't the same for all instances of {kib}. + +| `xpack.security.secureCookies` + | Sets the `secure` flag of the session cookie. The default value is `false`. It + is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set + this to `true` if SSL is configured outside of {kib} (for example, you are + routing requests through a load balancer or proxy). + +| `xpack.security.session.idleTimeout` + | Sets the session duration. By default, sessions stay active until the + browser is closed. When this is set to an explicit idle timeout, closing the + browser still requires the user to log back in to {kib}. + +|=== + +[TIP] +============ +The format is a string of `[ms|s|m|h|d|w|M|Y]` +(e.g. '70ms', '5s', '3d', '1Y'). +============ + +[cols="2*<"] +|=== + +| `xpack.security.session.lifespan` + | Sets the maximum duration, also known as "absolute timeout". By default, + a session can be renewed indefinitely. When this value is set, a session will end + once its lifespan is exceeded, even if the user is not idle. NOTE: if `idleTimeout` + is not set, this setting will still cause sessions to expire. + +|=== + +[TIP] +============ +The format is a +string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). +============ + +[cols="2*<"] +|=== + +| `xpack.security.loginAssistanceMessage` + | Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. + +|=== diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index bb0a15b29a087..bda5f00f762cd 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -5,18 +5,22 @@ Spaces settings ++++ -By default, Spaces is enabled in Kibana, and you can secure Spaces using +By default, Spaces is enabled in Kibana, and you can secure Spaces using roles when Security is enabled. [float] [[spaces-settings]] ==== Spaces settings -`xpack.spaces.enabled`:: -Set to `true` (default) to enable Spaces in {kib}. +[cols="2*<"] +|=== +| `xpack.spaces.enabled` + | Set to `true` (default) to enable Spaces in {kib}. -`xpack.spaces.maxSpaces`:: -The maximum amount of Spaces that can be used with this instance of Kibana. Some operations -in Kibana return all spaces using a single `_search` from Elasticsearch, so this must be -set lower than the `index.max_result_window` in Elasticsearch. -Defaults to `1000`. \ No newline at end of file +| `xpack.spaces.maxSpaces` + | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations + in {kib} return all spaces using a single `_search` from {es}, so this must be + set lower than the `index.max_result_window` in {es}. + Defaults to `1000`. + +|=== diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc index ad5f53ad879f8..33f167b13b310 100644 --- a/docs/settings/telemetry-settings.asciidoc +++ b/docs/settings/telemetry-settings.asciidoc @@ -8,7 +8,7 @@ By default, Usage Collection (also known as Telemetry) is enabled. This helps us learn about the {kib} features that our users are most interested in, so we can focus our efforts on making them even better. -You can control whether this data is sent from the {kib} servers, or if it should be sent +You can control whether this data is sent from the {kib} servers, or if it should be sent from the user's browser, in case a firewall is blocking the connections from the server. Additionally, you can decide to completely disable this feature either in the config file or in {kib} via *Management > Kibana > Advanced Settings > Usage Data*. See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to learn more. @@ -17,22 +17,30 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea [[telemetry-general-settings]] ==== General telemetry settings -`telemetry.enabled`:: *Default: true*. -Set to `true` to send cluster statistics to Elastic. Reporting your -cluster statistics helps us improve your user experience. Your data is never -shared with anyone. Set to `false` to disable statistics reporting from any -browser connected to the {kib} instance. - -`telemetry.sendUsageFrom`:: *Default: 'browser'*. -Set to `'server'` to report the cluster statistics from the {kib} server. -If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes -it is behind a firewall and falls back to `'browser'` to send it from users' browsers -when they are navigating through {kib}. - -`telemetry.optIn`:: *Default: true*. -Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through -*Advanced Settings* in {kib}. - -`telemetry.allowChangingOptInStatus`:: *Default: true*. -Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. -Note: When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +[cols="2*<"] +|=== +| `telemetry.enabled` + | Set to `true` to send cluster statistics to Elastic. Reporting your + cluster statistics helps us improve your user experience. Your data is never + shared with anyone. Set to `false` to disable statistics reporting from any + browser connected to the {kib} instance. Defaults to `true`. + +| `telemetry.sendUsageFrom` + | Set to `'server'` to report the cluster statistics from the {kib} server. + If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes + it is behind a firewall and falls back to `'browser'` to send it from users' browsers + when they are navigating through {kib}. Defaults to 'browser'. + +| `telemetry.optIn` + | Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through + *Advanced Settings* in {kib}. Defaults to `true`. + +| `telemetry.allowChangingOptInStatus` + | Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. Defaults to `true`. + + +|=== + +[NOTE] +============ +When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +============ diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 41fe8d337c03b..cc662af08b8f1 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -1,7 +1,7 @@ [[settings]] -== Configuring Kibana +== Configuring {kib} -The Kibana server reads properties from the `kibana.yml` file on startup. The +The {kib} server reads properties from the `kibana.yml` file on startup. The location of this file differs depending on how you installed {kib}. For example, if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by default it is in `$KIBANA_HOME/config`. By default, with package distributions @@ -11,444 +11,622 @@ The default host and port settings configure {kib} to run on `localhost:5601`. T variety of other options. Finally, environment variables can be injected into configuration using `${MY_ENV_VAR}` syntax. -.Kibana configuration settings +[cols="2*<"] +|=== -`console.enabled:`:: *Default: true* Set to false to disable Console. Toggling -this will cause the server to regenerate assets on the next startup, which may -cause a delay before pages start being served. +| `console.enabled:` + | Toggling this causes the server to regenerate assets on the next startup, +which may cause a delay before pages start being served. +Set to `false` to disable Console. *Default: `true`* -`cpu.cgroup.path.override:`:: Override for cgroup cpu path when mounted in a -manner that is inconsistent with `/proc/self/cgroup` +| `cpu.cgroup.path.override:` + | Override for cgroup cpu path when mounted in a +manner that is inconsistent with `/proc/self/cgroup`. -`cpuacct.cgroup.path.override:`:: Override for cgroup cpuacct path when mounted -in a manner that is inconsistent with `/proc/self/cgroup` +| `cpuacct.cgroup.path.override:` + | Override for cgroup cpuacct path when mounted +in a manner that is inconsistent with `/proc/self/cgroup`. -`csp.rules:`:: A template -https://w3c.github.io/webappsec-csp/[content-security-policy] that disables -certain unnecessary and potentially insecure capabilities in the browser. We -strongly recommend that you keep the default CSP rules that ship with Kibana. +| `csp.rules:` + | A https://w3c.github.io/webappsec-csp/[content-security-policy] template +that disables certain unnecessary and potentially insecure capabilities in +the browser. It is strongly recommended that you keep the default CSP rules +that ship with {kib}. -`csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that -does not enforce even rudimentary CSP rules. In practice, this will disable +| `csp.strict:` + | Blocks {kib} access to any browser that +does not enforce even rudimentary CSP rules. In practice, this disables support for older, less safe browsers like Internet Explorer. -See <> for more information. - -`csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after -loading Kibana to any browser that does not enforce even rudimentary CSP rules, -though Kibana is still accessible. This configuration is effectively ignored -when `csp.strict` is enabled. - -`elasticsearch.customHeaders:`:: *Default: `{}`* Header names and values to send -to Elasticsearch. Any custom headers cannot be overwritten by client-side -headers, regardless of the `elasticsearch.requestHeadersWhitelist` configuration. - -`elasticsearch.hosts:`:: *Default: `[ "http://localhost:9200" ]`* The URLs of the {es} instances to use for all your queries. All nodes -listed here must be on the same cluster. +For more information, refer to <>. +*Default: `true`* + +| `csp.warnLegacyBrowsers:` + | Shows a warning message after loading {kib} to any browser that does not +enforce even rudimentary CSP rules, though {kib} is still accessible. This +configuration is effectively ignored when `csp.strict` is enabled. +*Default: `true`* + +| `elasticsearch.customHeaders:` + | Header names and values to send to {es}. Any custom headers cannot be +overwritten by client-side headers, regardless of the +`elasticsearch.requestHeadersWhitelist` configuration. *Default: `{}`* + +| `elasticsearch.hosts:` + | The URLs of the {es} instances to use for all your queries. All nodes +listed here must be on the same cluster. *Default: `[ "http://localhost:9200" ]`* + -To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. - -`elasticsearch.logQueries:`:: *Default: `false`* Logs queries sent to -Elasticsearch. Requires `logging.verbose` set to `true`. This is useful for -seeing the query DSL generated by applications that currently do not have an -inspector, for example Timelion and Monitoring. - -`elasticsearch.pingTimeout:`:: -*Default: the value of the `elasticsearch.requestTimeout` setting* Time in -milliseconds to wait for Elasticsearch to respond to pings. - -`elasticsearch.preserveHost:`:: *Default: true* When this setting’s value is -true, Kibana uses the hostname specified in the `server.host` setting. When the -value of this setting is `false`, Kibana uses the hostname of the host that -connects to this Kibana instance. - -`elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List -of Kibana client-side headers to send to Elasticsearch. To send *no* client-side -headers, set this value to [] (an empty list). -Removing the `authorization` header from being whitelisted means that you cannot -use <> in Kibana. - -`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait -for responses from the back end or Elasticsearch. This value must be a positive -integer. - -`elasticsearch.shardTimeout:`:: *Default: 30000* Time in milliseconds for -Elasticsearch to wait for responses from shards. Set to 0 to disable. - -`elasticsearch.sniffInterval:`:: *Default: false* Time in milliseconds between -requests to check Elasticsearch for an updated list of nodes. - -`elasticsearch.sniffOnStart:`:: *Default: false* Attempt to find other -Elasticsearch nodes on startup. - -`elasticsearch.sniffOnConnectionFault:`:: *Default: false* Update the list of -Elasticsearch nodes immediately following a connection fault. - -`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls {kib}'s behavior in regard to presenting a client certificate when -requested by {es}. This setting applies to all outbound SSL/TLS connections to {es}, including requests that are proxied for end users. -+ -WARNING: If {es} uses certificates to authenticate end users with a PKI realm and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, -proxied requests may be executed as the identity that is tied to the {kib} server. - -`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 client certificate and its corresponding -private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take -effect, the `xpack.security.http.ssl.client_authentication` setting in {es} must be also be set to `"required"` or `"optional"` to request a -client certificate from {kib}. +To enable SSL/TLS for outbound connections to {es}, use the `https` protocol +in this setting. + +| `elasticsearch.logQueries:` + | Log queries sent to {es}. Requires `logging.verbose` set to `true`. +This is useful for seeing the query DSL generated by applications that +currently do not have an inspector, for example Timelion and Monitoring. +*Default: `false`* + +| `elasticsearch.pingTimeout:` + | Time in milliseconds to wait for {es} to respond to pings. +*Default: the value of the `elasticsearch.requestTimeout` setting* + +| `elasticsearch.preserveHost:` + | When the value is `true`, {kib} uses the hostname specified in the +`server.host` setting. When the value is `false`, {kib} uses +the hostname of the host that connects to this {kib} instance. *Default: `true`* + +| `elasticsearch.requestHeadersWhitelist:` + | List of {kib} client-side headers to send to {es}. To send *no* client-side +headers, set this value to [] (an empty list). Removing the `authorization` +header from being whitelisted means that you cannot use +<> in {kib}. +*Default: `[ 'authorization' ]`* + +| `elasticsearch.requestTimeout:` + | Time in milliseconds to wait for responses from the back end or {es}. +This value must be a positive integer. *Default: `30000`* + +| `elasticsearch.shardTimeout:` + | Time in milliseconds for {es} to wait for responses from shards. +Set to 0 to disable. *Default: `30000`* + +| `elasticsearch.sniffInterval:` + | Time in milliseconds between requests to check {es} for an updated list of +nodes. *Default: `false`* + +| `elasticsearch.sniffOnStart:` + | Attempt to find other {es} nodes on startup. *Default: `false`* + +| `elasticsearch.sniffOnConnectionFault:` + | Update the list of {es} nodes immediately following a connection fault. +*Default: `false`* + +| `elasticsearch.ssl.alwaysPresentCertificate:` + | Controls {kib} behavior in regard to presenting a client certificate when +requested by {es}. This setting applies to all outbound SSL/TLS connections +to {es}, including requests that are proxied for end users. *Default: `false`* + +|=== + +[WARNING] +============ +When {es} uses certificates to authenticate end users with a PKI realm +and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, +proxied requests may be executed as the identity that is tied to the {kib} +server. +============ + +[cols="2*<"] +|=== + +| `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` + | Paths to a PEM-encoded X.509 client certificate and its corresponding +private key. These are used by {kib} to authenticate itself when making +outbound SSL/TLS connections to {es}. For this setting to take effect, the +`xpack.security.http.ssl.client_authentication` setting in {es} must be also +be set to `"required"` or `"optional"` to request a client certificate from +{kib}. + +|=== + +[NOTE] +============ +These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +============ + +[cols="2*<"] +|=== + +| `elasticsearch.ssl.certificateAuthorities:` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) +certificates, which make up a trusted certificate chain for {es}. This chain is +used by {kib} to establish trust when making outbound SSL/TLS connections to +{es}. + -NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. - -`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a -trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. +In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. + +| `elasticsearch.ssl.keyPassphrase:` + | The password that decrypts the private key that is specified +via `elasticsearch.ssl.key`. This value is optional, as the key may not be +encrypted. + +| `elasticsearch.ssl.keystore.path:` + | Path to a PKCS#12 keystore that contains an X.509 client certificate and it's +corresponding private key. These are used by {kib} to authenticate itself when +making outbound SSL/TLS connections to {es}. For this setting, you must also set +the `xpack.security.http.ssl.client_authentication` setting in {es} to +`"required"` or `"optional"` to request a client certificate from {kib}. + -In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.keystore.path` and/or +If the keystore contains any additional certificates, they are used as a +trusted certificate chain for {es}. This chain is used by {kib} to establish +trust when making outbound SSL/TLS connections to {es}. In addition to this +setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. -`elasticsearch.ssl.keyPassphrase:`:: The password that will be used to decrypt the private key that is specified via -`elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. +|=== -`elasticsearch.ssl.keystore.path:`:: Path to a PKCS#12 keystore that contains an X.509 client certificate and its corresponding private key. -These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take effect, the -`xpack.security.http.ssl.client_authentication` setting in {es} must also be set to `"required"` or `"optional"` to request a client -certificate from {kib}. -+ --- -If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {es}. This chain is used by -{kib} to establish trust when making outbound SSL/TLS connections to {es}. In addition to this setting, trusted certificates may be -specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. +[NOTE] +============ +This setting cannot be used in conjunction with +`elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +============ -NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. --- +[cols="2*<"] +|=== -`elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the keystore that is specified via -`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to +| `elasticsearch.ssl.keystore.password:` + | The password that decrypts the keystore specified via +`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this +as blank. If the keystore has an empty password, set this to `""`. -`elasticsearch.ssl.truststore.path:`:: Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates -which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections -to {es}. +| `elasticsearch.ssl.truststore.path:`:: + | Path to a PKCS#12 trust store that contains one or more X.509 certificate +authority (CA) certificates, which make up a trusted certificate chain for +{es}. This chain is used by {kib} to establish trust when making outbound +SSL/TLS connections to {es}. + -In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or +In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. -`elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store specified via -`elasticsearch.ssl.truststore.path`. If the trust store has no password, leave this unset. If the trust store has an empty password, set -this to `""`. - -`elasticsearch.ssl.verificationMode:`:: *Default: `"full"`* Controls the verification of the server certificate that {kib} receives when -making an outbound SSL/TLS connection to {es}. Valid values are `"full"`, `"certificate"`, and `"none"`. Using `"full"` will perform -hostname verification, using `"certificate"` will skip hostname verification, and using `"none"` will skip verification entirely. - -`elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait -for Elasticsearch at Kibana startup before retrying. - -`elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch -is protected with basic authentication, these settings provide the username and -password that the Kibana server uses to perform maintenance on the Kibana index -at startup. Your Kibana users still need to authenticate with Elasticsearch, -which is proxied through the Kibana server. - -`interpreter.enableInVisualize`:: *Default: true* Enables use of interpreter in -Visualize. - -`kibana.defaultAppId:`:: *Default: "home"* The default application to load. - -`kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to -store saved searches, visualizations and dashboards. Kibana creates a new index -if the index doesn’t already exist. If you configure a custom index, the name must -be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations]. - -`kibana.autocompleteTimeout:`:: *Default: "1000"* Time in milliseconds to wait -for autocomplete suggestions from Elasticsearch. This value must be a whole number -greater than zero. - -`kibana.autocompleteTerminateAfter:`:: *Default: "100000"* Maximum number of -documents loaded by each shard to generate autocomplete suggestions. This value -must be a whole number greater than zero. - -`logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana -stores log output. - -`logging.json:`:: *Default: false* Logs output as JSON. When set to `true`, the -logs will be formatted as JSON strings that include timestamp, log level, context, message -text and any other metadata that may be associated with the log message itself. -If `logging.dest.stdout` is set and there is no interactive terminal ("TTY"), this setting -will default to `true`. - -`logging.quiet:`:: *Default: false* Set the value of this setting to `true` to -suppress all logging output other than error messages. - -`logging.rotate:`:: [experimental] Specifies the options for the logging rotate feature. +|`elasticsearch.ssl.truststore.password:` + | The password that decrypts the trust store specified via +`elasticsearch.ssl.truststore.path`. If the trust store has no password, +leave this as blank. If the trust store has an empty password, set this to `""`. + +| `elasticsearch.ssl.verificationMode:` + | Controls the verification of the server certificate that {kib} receives when +making an outbound SSL/TLS connection to {es}. Valid values are `"full"`, +`"certificate"`, and `"none"`. Using `"full"` performs hostname verification, +using `"certificate"` skips hostname verification, and using `"none"` skips +verification entirely. *Default: `"full"`* + +| `elasticsearch.startupTimeout:` + | Time in milliseconds to wait for {es} at {kib} startup before retrying. +*Default: `5000`* + +| `elasticsearch.username:` and `elasticsearch.password:` + | If your {es} is protected with basic authentication, these settings provide +the username and password that the {kib} server uses to perform maintenance +on the {kib} index at startup. {kib} users still need to authenticate with +{es}, which is proxied through the {kib} server. + +| `interpreter.enableInVisualize` + | Enables use of interpreter in Visualize. *Default: `true`* + +| `kibana.defaultAppId:` + | The default application to load. *Default: `"home"`* + +| `kibana.index:` + | {kib} uses an index in {es} to store saved searches, visualizations, and +dashboards. {kib} creates a new index if the index doesn’t already exist. +If you configure a custom index, the name must be lowercase, and conform to the +{es} {ref}/indices-create-index.html[index name limitations]. +*Default: `".kibana"`* + +| `kibana.autocompleteTimeout:` + | Time in milliseconds to wait for autocomplete suggestions from {es}. +This value must be a whole number greater than zero. *Default: `"1000"`* + +| `kibana.autocompleteTerminateAfter:` + | Maximum number of documents loaded by each shard to generate autocomplete +suggestions. This value must be a whole number greater than zero. +*Default: `"100000"`* + +| `logging.dest:` + | Enables you to specify a file where {kib} stores log output. +*Default: `stdout`* + +| `logging.json:` + | Logs output as JSON. When set to `true`, the logs are formatted as JSON +strings that include timestamp, log level, context, message text, and any other +metadata that may be associated with the log message. +When `logging.dest.stdout` is set, and there is no interactive terminal ("TTY"), +this setting defaults to `true`. *Default: `false`* + +| `logging.quiet:` + | Set the value of this setting to `true` to suppress all logging output other +than error messages. *Default: `false`* + +| `logging.rotate:` + | experimental[] Specifies the options for the logging rotate feature. When not defined, all the sub options defaults would be applied. The following example shows a valid logging rotate configuration: -+ + +|=== + +[source,text] -- - logging.rotate: - enabled: true - everyBytes: 10485760 - keepFiles: 10 + logging.rotate: + enabled: true + everyBytes: 10485760 + keepFiles: 10 -- -`logging.rotate.enabled:`:: [experimental] *Default: false* Set the value of this setting to `true` to +[cols="2*<"] +|=== + +| `logging.rotate.enabled:` + | experimental[] Set the value of this setting to `true` to enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` -that feature would not take any effect. +that feature would not take any effect. *Default: `false`* -`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the +| `logging.rotate.everyBytes:` + | experimental[] The maximum size of a log file (that is `not an exact` limit). After the limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and -this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). +this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). *Default: `10485760`* -`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep +| `logging.rotate.keepFiles:` + | experimental[] The number of most recent rotated log files to keep on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` -option has to be in the range of 2 to 1024 files. +option has to be in the range of 2 to 1024 files. *Default: `7`* -`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case -the `logging.rotate.usePolling` is enabled. That option has to be in the range of 5000 to 3600000 milliseconds. +| `logging.rotate.pollingInterval:` + | experimental[] The number of milliseconds for the polling strategy in case +the `logging.rotate.usePolling` is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* -`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring +| `logging.rotate.usePolling:` + | experimental[] By default we try to understand the best way to monitoring the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, -the `polling` method could be used enabling that option. +the `polling` method could be used enabling that option. *Default: `false`* -`logging.silent:`:: *Default: false* Set the value of this setting to `true` to -suppress all logging output. +| `logging.silent:` + | Set the value of this setting to `true` to +suppress all logging output. *Default: `false`* -`logging.timezone`:: *Default: UTC* Set to the canonical timezone id -(for example, `America/Los_Angeles`) to log events using that timezone. A list of timezones can -be referenced at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. +| `logging.timezone` + | Set to the canonical timezone ID +(for example, `America/Los_Angeles`) to log events using that timezone. For a +list of timezones, refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. *Default: `UTC`* -[[logging-verbose]]`logging.verbose:`:: *Default: false* Set the value of this -setting to `true` to log all events, including system usage information and all -requests. Supported on Elastic Cloud Enterprise. +| [[logging-verbose]] `logging.verbose:` + | Set to `true` to log all events, including system usage information and all +requests. Supported on {ece}. *Default: `false`* -`map.includeElasticMapsService:`:: *Default: true* -Set to false to disable connections to Elastic Maps Service. +| `map.includeElasticMapsService:` + | Set to `false` to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` will be available in <>. +and the tile layer configured by `map.tilemap.url` are available in <>. *Default: `true`* -`map.proxyElasticMapsServiceInMaps:`:: *Default: false* -Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. +| `map.proxyElasticMapsServiceInMaps:` + | Set to `true` to proxy all <> Elastic Maps Service +requests through the {kib} server. *Default: `false`* -[[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for +| [[regionmap-settings]] `map.regionmap:` + | Specifies additional vector layers for use in <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] and only include polygons. If the file is hosted on a separate domain from -Kibana, the server needs to be CORS-enabled so Kibana can download the file. -[[region-map-configuration-example]] +{kib}, the server needs to be CORS-enabled so {kib} can download the file. The following example shows a valid region map configuration. -+ + +|=== + +[source,text] -- - map - includeElasticMapsService: false - regionmap: - layers: - - name: "Departments of France" - url: "http://my.cors.enabled.server.org/france_departements.geojson" - attribution: "INRAP" - fields: - - name: "department" - description: "Full department name" - - name: "INSEE" - description: "INSEE numeric identifier" +map.regionmap: + includeElasticMapsService: false + layers: + - name: "Departments of France" + url: "http://my.cors.enabled.server.org/france_departements.geojson" + attribution: "INRAP" + fields: + - name: "department" + description: "Full department name" + - name: "INSEE" + description: "INSEE numeric identifier" -- -[[regionmap-ES-map]]`map.includeElasticMapsService:`:: Turns on or off -whether layers from the Elastic Maps Service should be included in the vector -layer option list. Supported on Elastic Cloud Enterprise. By turning this off, +[cols="2*<"] +|=== + +| [[regionmap-ES-map]] `map.includeElasticMapsService:` + | Turns on or off whether layers from the Elastic Maps Service should be included in the vector +layer option list. Supported on {ece}. By turning this off, only the layers that are configured here will be included. The default is `true`. This also affects whether tile-service from the Elastic Maps Service will be available. -[[regionmap-attribution]]`map.regionmap.layers[].attribution:`:: Optional. -References the originating source of the geojson file. Supported on {ece}. +| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` + | Optional. References the originating source of the geojson file. +Supported on {ece}. -[[regionmap-fields]]`map.regionmap.layers[].fields[]:`:: Mandatory. Each layer +| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` + | Mandatory. Each layer can contain multiple fields to indicate what properties from the geojson -features you wish to expose. This <> shows how to define multiple -properties. Supported on {ece}. +features you wish to expose. Supported on {ece}. The following shows how to define multiple +properties: + +|=== -[[regionmap-field-description]]`map.regionmap.layers[].fields[].description:`:: -Mandatory. The human readable text that is shown under the Options tab when +[source,text] +-- +map.regionmap: + includeElasticMapsService: false + layers: + - name: "Departments of France" + url: "http://my.cors.enabled.server.org/france_departements.geojson" + attribution: "INRAP" + fields: + - name: "department" + description: "Full department name" + - name: "INSEE" + description: "INSEE numeric identifier" +-- + +[cols="2*<"] +|=== + +| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` + | Mandatory. The human readable text that is shown under the Options tab when building the Region Map visualization. Supported on {ece}. -[[regionmap-field-name]]`map.regionmap.layers[].fields[].name:`:: Mandatory. +| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` + | Mandatory. This value is used to do an inner-join between the document stored in -Elasticsearch and the geojson file. For example, if the field in the geojson is -called `Location` and has city names, there must be a field in Elasticsearch -that holds the same values that Kibana can then use to lookup for the geoshape +{es} and the geojson file. For example, if the field in the geojson is +called `Location` and has city names, there must be a field in {es} +that holds the same values that {kib} can then use to lookup for the geoshape data. Supported on {ece}. -[[regionmap-name]]`map.regionmap.layers[].name:`:: Mandatory. A description of +| [[regionmap-name]] `map.regionmap.layers[].name:` + | Mandatory. A description of the map being provided. Supported on {ece}. -[[regionmap-url]]`map.regionmap.layers[].url:`:: Mandatory. The location of the +| [[regionmap-url]] `map.regionmap.layers[].url:` + | Mandatory. The location of the geojson file as provided by a webserver. Supported on {ece}. -[[tilemap-settings]] `map.tilemap.options.attribution:`:: +| [[tilemap-settings]] `map.tilemap.options.attribution:` + | The map attribution string. Supported on {ece}. *Default: `"© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)"`* -The map attribution string. Supported on {ece}. -[[tilemap-max-zoom]]`map.tilemap.options.maxZoom:`:: *Default: 10* The maximum -zoom level. Supported on {ece}. +| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` + | The maximum zoom level. Supported on {ece}. *Default: `10`* -[[tilemap-min-zoom]]`map.tilemap.options.minZoom:`:: *Default: 1* The minimum -zoom level. Supported on {ece}. +| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` + | The minimum zoom level. Supported on {ece}. *Default: `1`* -[[tilemap-subdomains]]`map.tilemap.options.subdomains:`:: An array of subdomains +| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` + | An array of subdomains used by the tile service. Specify the position of the subdomain the URL with the token `{s}`. Supported on {ece}. -[[tilemap-url]]`map.tilemap.url:`:: The URL to the tileservice that Kibana uses +| [[tilemap-url]] `map.tilemap.url:` + | The URL to the tileservice that {kib} uses to display map tiles in tilemap visualizations. Supported on {ece}. By default, -Kibana reads this url from an external metadata service, but users can still +{kib} reads this URL from an external metadata service, but users can override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` -`newsfeed.enabled:` :: *Default: `true`* Controls whether to enable the newsfeed -system for the Kibana UI notification center. Set to `false` to disable the -newsfeed system. +| `newsfeed.enabled:` + | Controls whether to enable the newsfeed +system for the {kib} UI notification center. Set to `false` to disable the +newsfeed system. *Default: `true`* -`path.data:`:: *Default: `data`* The path where Kibana stores persistent data -not saved in Elasticsearch. +| `path.data:` + | The path where {kib} stores persistent data +not saved in {es}. *Default: `data`* -`pid.file:`:: Specifies the path where Kibana creates the process ID file. +| `pid.file:` + | Specifies the path where {kib} creates the process ID file. -`ops.interval:`:: *Default: 5000* Set the interval in milliseconds to sample -system and process performance metrics. The minimum value is 100. +| `ops.interval:` + | Set the interval in milliseconds to sample +system and process performance metrics. The minimum value is 100. *Default: `5000`* -`server.basePath:`:: Enables you to specify a path to mount Kibana at if you are -running behind a proxy. Use the `server.rewriteBasePath` setting to tell Kibana +| `server.basePath:` + | Enables you to specify a path to mount {kib} at if you are +running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (`/`). -[[server-compression]]`server.compression.enabled:`:: *Default: `true`* Set to `false` to disable HTTP compression for all responses. +| [[server-compression]] `server.compression.enabled:` + | Set to `false` to disable HTTP compression for all responses. *Default: `true`* -`server.compression.referrerWhitelist:`:: *Default: none* Specifies an array of trusted hostnames, such as the Kibana host, or a reverse -proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request's `Referer` header. -This setting may not be used when `server.compression.enabled` is set to `false`. +| `server.compression.referrerWhitelist:` + | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse +proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. +This setting may not be used when `server.compression.enabled` is set to `false`. *Default: `none`* -`server.customResponseHeaders:`:: *Default: `{}`* Header names and values to - send on all responses to the client from the Kibana server. +| `server.customResponseHeaders:` + | Header names and values to +send on all responses to the client from the {kib} server. *Default: `{}`* -`server.host:`:: *Default: "localhost"* This setting specifies the host of the -back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. +| `server.host:` + | This setting specifies the host of the +back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. *Default: `"localhost"`* -`server.keepaliveTimeout:`:: *Default: "120000"* The number of milliseconds to wait for additional data before restarting -the `server.socketTimeout` counter. +| `server.keepaliveTimeout:` + | The number of milliseconds to wait for additional data before restarting +the `server.socketTimeout` counter. *Default: `"120000"`* -`server.maxPayloadBytes:`:: *Default: 1048576* The maximum payload size in bytes -for incoming server requests. +| `server.maxPayloadBytes:` + | The maximum payload size in bytes +for incoming server requests. *Default: `1048576`* -`server.name:`:: *Default: "your-hostname"* A human-readable display name that -identifies this Kibana instance. +| `server.name:` + | A human-readable display name that +identifies this {kib} instance. *Default: `"your-hostname"`* -`server.port:`:: *Default: 5601* Kibana is served by a back end server. This -setting specifies the port to use. +| `server.port:` + | {kib} is served by a back end server. This +setting specifies the port to use. *Default: `5601`* -`server.rewriteBasePath:`:: *Default: deprecated* Specifies whether Kibana should +| `server.rewriteBasePath:` + | Specifies whether {kib} should rewrite requests that are prefixed with `server.basePath` or require that they -are rewritten by your reverse proxy. In Kibana 6.3 and earlier, the default is -`false`. In Kibana 7.x, the setting is deprecated. In Kibana 8.0 and later, the -default is `true`. +are rewritten by your reverse proxy. In {kib} 6.3 and earlier, the default is +`false`. In {kib} 7.x, the setting is deprecated. In {kib} 8.0 and later, the +default is `true`. *Default: `deprecated`* -`server.socketTimeout:`:: *Default: "120000"* The number of milliseconds to wait before closing an -inactive socket. +| `server.socketTimeout:` + | The number of milliseconds to wait before closing an +inactive socket. *Default: `"120000"`* -`server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These -are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. -+ -NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`. +| `server.ssl.certificate:` and `server.ssl.key:` + | Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These +are used by {kib} to establish trust when receiving inbound SSL/TLS connections from users. + +|=== -`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a +[NOTE] +============ +These settings cannot be used in conjunction with `server.ssl.keystore.path`. +============ + +[cols="2*<"] +|=== + +| `server.ssl.certificateAuthorities:` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. -`server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. -Details on the format, and the valid options, are available via the +| `server.ssl.cipherSuites:` + | Details on the format, and the valid options, are available via the https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation]. +*Default: `ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA`*. -`server.ssl.clientAuthentication:`:: *Default: `"none"`* Controls {kib}’s behavior in regard to requesting a certificate from client +| `server.ssl.clientAuthentication:` + | Controls the behavior in {kib} for requesting a certificate from client connections. Valid values are `"required"`, `"optional"`, and `"none"`. Using `"required"` will refuse to establish the connection unless a client presents a certificate, using `"optional"` will allow a client to present a certificate if it has one, and using `"none"` will -prevent a client from presenting a certificate. +prevent a client from presenting a certificate. *Default: `"none"`* -`server.ssl.enabled:`:: *Default: `false`* Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its +| `server.ssl.enabled:` + | Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its corresponding private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of -`server.ssl.certificate` and `server.ssl.key`. +`server.ssl.certificate` and `server.ssl.key`. *Default: `false`* -`server.ssl.keyPassphrase:`:: The password that will be used to decrypt the private key that is specified via `server.ssl.key`. This value +| `server.ssl.keyPassphrase:` + | The password that decrypts the private key that is specified via `server.ssl.key`. This value is optional, as the key may not be encrypted. -`server.ssl.keystore.path:`:: Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the +| `server.ssl.keystore.path:` + | Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {kib}. All of these are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {kib} to verify client certificates from end users when PKI authentication is enabled. + --- In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. -NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`. --- +|=== + +[NOTE] +============ +This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key` +============ -`server.ssl.keystore.password:`:: The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the +[cols="2*<"] +|=== + +| `server.ssl.keystore.password:` + | The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to `""`. -`server.ssl.truststore.path:`:: Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which +| `server.ssl.truststore.path:` + | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or `server.ssl.keystore.path`. -`server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If +| `server.ssl.truststore.password:` + | The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If the trust store has no password, leave this unset. If the trust store has an empty password, set this to `""`. -`server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect +| `server.ssl.redirectHttpFromPort:` + | {kib} binds to this port and redirects all http requests to https over the port configured as `server.port`. -`server.ssl.supportedProtocols:`:: *Default: TLSv1.1, TLSv1.2* An array of -supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2` +| `server.ssl.supportedProtocols:` + | An array of supported protocols with versions. +Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: TLSv1.1, TLSv1.2* -`server.xsrf.whitelist:`:: It is not recommended to disable protections for +| `server.xsrf.whitelist:` + | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. The `server.xsrf.whitelist` setting requires the following format: -[source,text] +|=== +[source,text] ---- *Default: [ ]* An array of API endpoints which should be exempt from Cross-Site Request Forgery ("XSRF") protections. ---- -`status.allowAnonymous:`:: *Default: false* If authentication is enabled, -setting this to `true` enables unauthenticated users to access the Kibana -server status API and status page. +[cols="2*<"] +|=== + +| `status.allowAnonymous:` + | If authentication is enabled, +setting this to `true` enables unauthenticated users to access the {kib} +server status API and status page. *Default: `false`* -`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, -users are able to change the telemetry setting at a later time in -<>. If `false`, +| `telemetry.allowChangingOptInStatus` + | When `true`, users are able to change the telemetry setting at a later time in +<>. When `false`, {kib} looks at the value of `telemetry.optIn` to determine whether to send telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` -cannot be `false` at the same time. +cannot be `false` at the same time. *Default: `true`*. -`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. - If `false`, collection of telemetry data is disabled. - To enable telemetry and prevent users from disabling it, - set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +| `telemetry.optIn` + | When `true`, telemetry data is sent to Elastic. +When `false`, collection of telemetry data is disabled. +To enable telemetry and prevent users from disabling it, +set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +*Default: `true`* -`telemetry.enabled`:: *Default: true* Reporting your cluster statistics helps +| `telemetry.enabled` + | Reporting your cluster statistics helps us improve your user experience. Your data is never shared with anyone. Set to `false` to disable telemetry capabilities entirely. You can alternatively opt -out through the *Advanced Settings* in {kib}. +out through *Advanced Settings*. *Default: `true`* + +| `vis_type_vega.enableExternalUrls:` + | Set this value to true to allow Vega to use any URL to access external data +sources and images. When false, Vega can only get data from {es}. *Default: `false`* -`vis_type_vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. +| `xpack.license_management.enabled` + | Set this value to false to +disable the License Management UI. *Default: `true`* -`xpack.license_management.enabled`:: *Default: true* Set this value to false to -disable the License Management user interface. +| `xpack.rollup.enabled:` + | Set this value to false to disable the +Rollup UI. *Default: true* -`xpack.rollup.enabled:`:: *Default: true* Set this value to false to disable the -Rollup user interface. +| `i18n.locale` + | Set this value to change the {kib} interface language. +Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* -`i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. +|=== include::{docdir}/settings/alert-action-settings.asciidoc[] include::{docdir}/settings/apm-settings.asciidoc[] From fb6d325fe92ab9426f6ca4108cad608cff88e592 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 4 May 2020 10:49:14 -0500 Subject: [PATCH 003/143] [DOCS} Fixes 404s in master (#64911) --- docs/redirects.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index a5503969a3ec1..85d580de9475f 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -70,3 +70,13 @@ This page has moved. Please see <>. == Maps This page has moved. Please see <>. + +[role="exclude",id="development-embedding-visualizations"] +== Embedding Visualizations + +This page was deleted. See <>. + +[role="exclude",id="development-create-visualization"] +== Developing Visualizations + +This page was deleted. See <>. From 86c64af553bec850358c9d86f588489f2d82ac12 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 4 May 2020 18:16:48 +0200 Subject: [PATCH 004/143] Bump mapbox-gl dependency from 1.9.0 to 1.10.0 (#64670) --- x-pack/package.json | 4 +- yarn.lock | 93 +++++++++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index dcc9b8c61cb96..5d1fbaa5784e0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -81,7 +81,7 @@ "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^7.2.8", "@types/lodash": "^3.10.1", - "@types/mapbox-gl": "^1.8.1", + "@types/mapbox-gl": "^1.9.1", "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", "@types/mocha": "^7.0.2", @@ -280,7 +280,7 @@ "lodash.topath": "^4.5.2", "lodash.uniqby": "^4.7.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.9.0", + "mapbox-gl": "^1.10.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "markdown-it": "^10.0.0", "memoize-one": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 346c4d76d24c9..3c233b76f1a48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2116,7 +2116,7 @@ resolved "https://registry.yarnpkg.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz#1da1e6b3a7add3ad29909b30f438f60581b7cd80" integrity sha1-HaHms6et060pkJsw9Dj2BYG3zYA= -"@mapbox/geojson-rewind@^0.4.0", "@mapbox/geojson-rewind@^0.4.1": +"@mapbox/geojson-rewind@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.4.1.tgz#357d79300adb7fec7c1f091512988bca6458f068" integrity sha512-mxo2MEr7izA1uOXcDsw99Kgg6xW3P4H2j4n1lmldsgviIelpssvP+jQDivFKOHrOVJDpTTi5oZJvRcHtU9Uufw== @@ -2126,6 +2126,14 @@ minimist "^1.2.5" sharkdown "^0.1.0" +"@mapbox/geojson-rewind@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz#91f0ad56008c120caa19414b644d741249f4f560" + integrity sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg== + dependencies: + concat-stream "~2.0.0" + minimist "^1.2.5" + "@mapbox/geojson-types@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz#9aecf642cb00eab1080a57c4f949a65b4a5846d6" @@ -2166,20 +2174,20 @@ resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz#a26ecfb3f0061456d93ee8570dd9587d226ea8bd" integrity sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw== -"@mapbox/mapbox-gl-supported@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.4.0.tgz#36946b22944fe2cfa43cfafd5ef36fdb54a069e4" - integrity sha512-ZD0Io4XK+/vU/4zpANjOtdWfVszAgnaMPsGR6LKsWh4kLIEv9qoobTVmJPPuwuM+ZI2b3BlZ6DYw1XHVmv6YTA== +"@mapbox/mapbox-gl-supported@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz#f60b6a55a5d8e5ee908347d2ce4250b15103dc8e" + integrity sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg== "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI= -"@mapbox/tiny-sdf@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-1.1.0.tgz#b0b8f5c22005e6ddb838f421ffd257c1f74f9a20" - integrity sha512-dnhyk8X2BkDRWImgHILYAGgo+kuciNYX30CUKj/Qd5eNjh54OWM/mdOS/PWsPeN+3abtN+QDGYM4G220ynVJKA== +"@mapbox/tiny-sdf@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-1.1.1.tgz#16a20c470741bfe9191deb336f46e194da4a91ff" + integrity sha512-Ihn1nZcGIswJ5XGbgFAvVumOgWpvIjBX9jiRlIl46uQG9vJOF51ViBYHF95rEZupuyQbEmhLaDPLQlU7fUTsBg== "@mapbox/unitbezier@^0.0.0": version "0.0.0" @@ -4350,10 +4358,10 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== -"@types/mapbox-gl@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.8.1.tgz#dbc12da1324d5bdb3dbf71b90b77cac17994a1a3" - integrity sha512-DdT/YzpGiYITkj2cUwyqPilPbtZURr1E0vZX0KTyyeNP0t0bxNyKoXo0seAcvUd2MsMgFYwFQh1WRC3x2V0kKQ== +"@types/mapbox-gl@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.9.1.tgz#78b62f8a1ead78bc525a4c1db84bb71fa0fcc579" + integrity sha512-5LS/fljbGjCPfjtOK5+pz8TT0PL4bBXTnN/PDbPtTQMqQdY/KWTWE4jRPuo0fL5wctd543DCptEUTydn+JK+gA== dependencies: "@types/geojson" "*" @@ -9539,6 +9547,16 @@ concat-stream@~1.5.0: readable-stream "~2.0.0" typedarray "~0.0.5" +concat-stream@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + conf@^1.1.2, conf@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/conf/-/conf-1.4.0.tgz#1ea66c9d7a9b601674a5bb9d2b8dc3c726625e67" @@ -10355,7 +10373,7 @@ css@2.X, css@^2.0.0, css@^2.2.1, css@^2.2.3, css@^2.2.4: source-map-resolve "^0.5.2" urix "^0.1.0" -csscolorparser@~1.0.2: +csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" integrity sha1-s085HupNqPPpgjHizNjfnAQfFxs= @@ -14586,10 +14604,10 @@ github-username@^3.0.0: dependencies: gh-got "^5.0.0" -gl-matrix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.0.0.tgz#888301ac7650e148c3865370e13ec66d08a8381f" - integrity sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA== +gl-matrix@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" + integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== glob-all@^3.1.0, glob-all@^3.2.1: version "3.2.1" @@ -20091,33 +20109,33 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.9.0.tgz#53e3e13c99483f362b07a8a763f2d61d580255a5" - integrity sha512-PKpoiB2pPUMrqFfBJpt/oA8On3zcp0adEoDS2YIC2RA6o4EZ9Sq2NPZocb64y7ra3mLUvEb7ps1pLVlPMh6y7w== +mapbox-gl@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.10.0.tgz#c33e74d1f328e820e245ff8ed7b5dbbbc4be204f" + integrity sha512-SrJXcR9s5yEsPuW2kKKumA1KqYW9RrL8j7ZcIh6glRQ/x3lwNMfwz/UEJAJcVNgeX+fiwzuBoDIdeGB/vSkZLQ== dependencies: - "@mapbox/geojson-rewind" "^0.4.0" + "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" "@mapbox/jsonlint-lines-primitives" "^2.0.2" - "@mapbox/mapbox-gl-supported" "^1.4.0" + "@mapbox/mapbox-gl-supported" "^1.5.0" "@mapbox/point-geometry" "^0.1.0" - "@mapbox/tiny-sdf" "^1.1.0" + "@mapbox/tiny-sdf" "^1.1.1" "@mapbox/unitbezier" "^0.0.0" "@mapbox/vector-tile" "^1.3.1" "@mapbox/whoots-js" "^3.1.0" - csscolorparser "~1.0.2" + csscolorparser "~1.0.3" earcut "^2.2.2" geojson-vt "^3.2.1" - gl-matrix "^3.0.0" + gl-matrix "^3.2.1" grid-index "^1.1.0" - minimist "0.0.8" + minimist "^1.2.5" murmurhash-js "^1.0.0" pbf "^3.2.1" potpack "^1.0.1" quickselect "^2.0.0" rw "^1.3.3" supercluster "^7.0.0" - tinyqueue "^2.0.0" + tinyqueue "^2.0.3" vt-pbf "^3.1.1" mapcap@^1.0.0: @@ -25121,6 +25139,15 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.1.0: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -29031,10 +29058,10 @@ tinymath@1.2.1: resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-1.2.1.tgz#f97ed66c588cdbf3c19dfba2ae266ee323db7e47" integrity sha512-8CYutfuHR3ywAJus/3JUhaJogZap1mrUQGzNxdBiQDhP3H0uFdQenvaXvqI8lMehX4RsanRZzxVfjMBREFdQaA== -tinyqueue@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.2.tgz#b4fe66d28a5b503edb99c149f87910059782a0cc" - integrity sha512-1oUV+ZAQaeaf830ui/p5JZpzGBw46qs1pKHcfqIc6/QxYDQuEmcBLIhiT0xAxLnekz+qxQusubIYk4cAS8TB2A== +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== title-case@^2.1.0: version "2.1.1" From 496f49247419d80ea3d400a35753ba499f2f2bc3 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 4 May 2020 18:39:09 +0200 Subject: [PATCH 005/143] Fix 37422 (#64215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 rename Filter -> ExpressionValueFilter * refactor: 💡 use new filter type in Canvas * fix: 🐛 fix tests after refactor Co-authored-by: Elastic Machine --- .../common/expression_types/specs/filter.ts | 32 ++++++++++--------- src/plugins/expressions/public/index.ts | 2 +- src/plugins/expressions/server/index.ts | 2 +- .../functions/common/exactly.test.js | 2 +- .../functions/common/exactly.ts | 14 +++++--- .../functions/common/saved_lens.test.ts | 15 +++++++-- .../functions/common/saved_lens.ts | 8 ++--- .../functions/common/saved_map.test.ts | 15 +++++++-- .../functions/common/saved_map.ts | 4 +-- .../functions/common/saved_search.test.ts | 15 +++++++-- .../functions/common/saved_search.ts | 4 +-- .../common/saved_visualization.test.ts | 15 +++++++-- .../functions/common/saved_visualization.ts | 4 +-- .../functions/common/timefilter.test.js | 2 +- .../functions/common/timefilter.ts | 11 ++++--- .../functions/server/demodata.test.ts | 7 ++-- .../functions/server/demodata/index.ts | 9 ++++-- .../functions/server/escount.ts | 15 +++++++-- .../functions/server/esdocs.ts | 12 +++++-- .../functions/server/essql.ts | 9 ++++-- .../canvas/common/lib/datatable/query.js | 4 +-- .../canvas/public/functions/filters.ts | 9 ++++-- .../canvas/public/functions/timelion.ts | 7 ++-- .../lib/build_embeddable_filters.test.ts | 12 ++++--- .../public/lib/build_embeddable_filters.ts | 13 ++++---- .../canvas/server/lib/get_es_filter.js | 8 ++--- x-pack/legacy/plugins/canvas/types/state.ts | 4 +-- 27 files changed, 165 insertions(+), 89 deletions(-) diff --git a/src/plugins/expressions/common/expression_types/specs/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts index 01d6b8a603db6..fc1c086e817c9 100644 --- a/src/plugins/expressions/common/expression_types/specs/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -17,29 +17,31 @@ * under the License. */ -import { ExpressionTypeDefinition } from '../types'; - -const name = 'filter'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; /** * Represents an object that is a Filter. */ -export interface Filter { - type?: string; - value?: string; - column?: string; - and: Filter[]; - to?: string; - from?: string; - query?: string | null; -} +export type ExpressionValueFilter = ExpressionValueBoxed< + 'filter', + { + filterType?: string; + value?: string; + column?: string; + and: ExpressionValueFilter[]; + to?: string; + from?: string; + query?: string | null; + } +>; -export const filter: ExpressionTypeDefinition = { - name, +export const filter: ExpressionTypeDefinition<'filter', ExpressionValueFilter> = { + name: 'filter', from: { null: () => { return { - type: name, + type: 'filter', + filterType: 'filter', // Any meta data you wish to pass along. meta: {}, // And filters. If you need an "or", create a filter type for it. diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 6814764ee5faa..ee3fbd7a7b0b0 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -78,7 +78,7 @@ export { ExpressionValueRender, ExpressionValueSearchContext, ExpressionValueUnboxed, - Filter, + ExpressionValueFilter, Font, FontLabel, FontStyle, diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index e41135b693922..61d3838466bef 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -69,7 +69,7 @@ export { ExpressionValueRender, ExpressionValueSearchContext, ExpressionValueUnboxed, - Filter, + ExpressionValueFilter, Font, FontLabel, FontStyle, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js index f03bc54757c3c..2b9bdb59afbdf 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js @@ -18,7 +18,7 @@ describe('exactly', () => { it("adds an exactly object to 'and'", () => { const result = fn(emptyFilter, { column: 'name', value: 'product2' }); - expect(result.and[0]).toHaveProperty('type', 'exactly'); + expect(result.and[0]).toHaveProperty('filterType', 'exactly'); }); describe('args', () => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index 88a24186d6044..5031e8029957b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter, ExpressionFunctionDefinition } from '../../../types'; +import { ExpressionValueFilter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,7 +13,12 @@ interface Arguments { filterGroup: string; } -export function exactly(): ExpressionFunctionDefinition<'exactly', Filter, Arguments, Filter> { +export function exactly(): ExpressionFunctionDefinition< + 'exactly', + ExpressionValueFilter, + Arguments, + ExpressionValueFilter +> { const { help, args: argHelp } = getFunctionHelp().exactly; return { @@ -43,8 +48,9 @@ export function exactly(): ExpressionFunctionDefinition<'exactly', Filter, Argum fn: (input, args) => { const { value, column } = args; - const filter = { - type: 'exactly', + const filter: ExpressionValueFilter = { + type: 'filter', + filterType: 'exactly', value, column, and: [], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts index 6b197148e6373..882d1e2ea58b9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedLens } from './saved_lens'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts index 2985a68cf855c..8fc55ddf9cc59 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; import { EmbeddableInput } from 'src/plugins/embeddable/public'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -37,7 +37,7 @@ type Return = EmbeddableExpression; export function savedLens(): ExpressionFunctionDefinition< 'savedLens', - Filter | null, + ExpressionValueFilter | null, Arguments, Return > { @@ -63,8 +63,8 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (context, args) => { - const filters = context ? context.and : []; + fn: (input, args) => { + const filters = input ? input.and : []; return { type: EmbeddableExpressionType, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts index 63dbae55790a3..74e41a030de35 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedMap } from './saved_map'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index cba19ce7da80f..df316d0dd182f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -6,7 +6,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -32,7 +32,7 @@ type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts index 67356dae5b3e3..9bd32202b563a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedSearch } from './saved_search'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts index 87dc7eb5e814c..277d035ed0958 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts @@ -13,7 +13,7 @@ import { } from '../../expression_types'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -24,7 +24,7 @@ type Output = EmbeddableExpression & { id: SearchInput['id' export function savedSearch(): ExpressionFunctionDefinition< 'savedSearch', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts index 754a113b87554..8327c1433b9af 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedVisualization } from './saved_visualization'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index d98fea2ec1be8..94c7a1c8a9eea 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -12,7 +12,7 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; +import { ExpressionValueFilter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -31,7 +31,7 @@ const defaultTimeRange = { export function savedVisualization(): ExpressionFunctionDefinition< 'savedVisualization', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js index aeab0d50c31a7..834b9d195856c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js @@ -44,7 +44,7 @@ describe('timefilter', () => { from: fromDate, to: toDate, }).and[0] - ).toHaveProperty('type', 'time'); + ).toHaveProperty('filterType', 'time'); }); describe('args', () => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index 249faf6141b46..ff7b56d8194df 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { Filter, ExpressionFunctionDefinition } from '../../../types'; +import { ExpressionValueFilter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -17,9 +17,9 @@ interface Arguments { export function timefilter(): ExpressionFunctionDefinition< 'timefilter', - Filter, + ExpressionValueFilter, Arguments, - Filter + ExpressionValueFilter > { const { help, args: argHelp } = getFunctionHelp().timefilter; const errors = getFunctionErrors().timefilter; @@ -58,8 +58,9 @@ export function timefilter(): ExpressionFunctionDefinition< } const { from, to, column } = args; - const filter: Filter = { - type: 'time', + const filter: ExpressionValueFilter = { + type: 'filter', + filterType: 'time', column, and: [], }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts index 94b2d5228665b..2b517664793a7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts @@ -5,12 +5,11 @@ */ import { demodata } from './demodata'; +import { ExpressionValueFilter } from '../../../types'; -const nullFilter = { +const nullFilter: ExpressionValueFilter = { type: 'filter', - meta: {}, - size: null, - sort: [], + filterType: 'filter', and: [], }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 5cebae5bb669f..843e2bda47e12 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -10,14 +10,19 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; import { queryDatatable } from '../../../../common/lib/datatable/query'; import { DemoRows } from './demo_rows_types'; import { getDemoRows } from './get_demo_rows'; -import { Filter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; +import { ExpressionValueFilter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; import { getFunctionHelp } from '../../../../i18n'; interface Arguments { type: string; } -export function demodata(): ExpressionFunctionDefinition<'demodata', Filter, Arguments, Datatable> { +export function demodata(): ExpressionFunctionDefinition< + 'demodata', + ExpressionValueFilter, + Arguments, + Datatable +> { const { help, args: argHelp } = getFunctionHelp().demodata; return { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts index ffb8bb4f3e2a7..3f5d0610b4c72 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunctionDefinition, Filter } from 'src/plugins/expressions/common'; +import { + ExpressionFunctionDefinition, + ExpressionValueFilter, +} from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { buildESRequest } from '../../../server/lib/build_es_request'; import { getFunctionHelp } from '../../../i18n'; @@ -14,7 +17,12 @@ interface Arguments { query: string; } -export function escount(): ExpressionFunctionDefinition<'escount', Filter, Arguments, any> { +export function escount(): ExpressionFunctionDefinition< + 'escount', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().escount; return { @@ -40,7 +48,8 @@ export function escount(): ExpressionFunctionDefinition<'escount', Filter, Argum fn: (input, args, handlers) => { input.and = input.and.concat([ { - type: 'luceneQueryString', + type: 'filter', + filterType: 'luceneQueryString', query: args.query, and: [], }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index 5bff06bb3933b..d60297ee2da3f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -8,7 +8,7 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -20,7 +20,12 @@ interface Arguments { count: number; } -export function esdocs(): ExpressionFunctionDefinition<'esdocs', Filter, Arguments, any> { +export function esdocs(): ExpressionFunctionDefinition< + 'esdocs', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().esdocs; return { @@ -67,7 +72,8 @@ export function esdocs(): ExpressionFunctionDefinition<'esdocs', Filter, Argumen input.and = input.and.concat([ { - type: 'luceneQueryString', + type: 'filter', + filterType: 'luceneQueryString', query: args.query, and: [], }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index cdb6b5af82015..b972f5a3bd4a6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -7,7 +7,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -16,7 +16,12 @@ interface Arguments { timezone: string; } -export function essql(): ExpressionFunctionDefinition<'essql', Filter, Arguments, any> { +export function essql(): ExpressionFunctionDefinition< + 'essql', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().essql; return { diff --git a/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js b/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js index f61e2b6434697..63945ce7690f9 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js +++ b/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js @@ -15,14 +15,14 @@ export function queryDatatable(datatable, query) { if (query.and) { query.and.forEach(filter => { // handle exact matches - if (filter.type === 'exactly') { + if (filter.filterType === 'exactly') { datatable.rows = datatable.rows.filter(row => { return row[filter.column] === filter.value; }); } // handle time filters - if (filter.type === 'time') { + if (filter.filterType === 'time') { const columnNames = datatable.columns.map(col => col.name); // remove row if no column match diff --git a/x-pack/legacy/plugins/canvas/public/functions/filters.ts b/x-pack/legacy/plugins/canvas/public/functions/filters.ts index 2a3bc481d7dae..16d0bb0fff708 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/filters.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/filters.ts @@ -11,7 +11,7 @@ import { interpretAst } from '../lib/run_interpreter'; // @ts-ignore untyped local import { getState } from '../state/store'; import { getGlobalFilters } from '../state/selectors/workpad'; -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from '.'; @@ -41,7 +41,12 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = }); } -type FiltersFunction = ExpressionFunctionDefinition<'filters', null, Arguments, Filter>; +type FiltersFunction = ExpressionFunctionDefinition< + 'filters', + null, + Arguments, + ExpressionValueFilter +>; export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { return function filters(): FiltersFunction { diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index e59d798108945..d07b3bf6d1d1c 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -11,7 +11,7 @@ import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressi import { fetch } from '../../common/lib/fetch'; // @ts-ignore untyped local import { buildBoolArray } from '../../server/lib/build_bool_array'; -import { Datatable, Filter } from '../../types'; +import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from './'; @@ -49,7 +49,7 @@ function parseDateMath( type TimelionFunction = ExpressionFunctionDefinition< 'timelion', - Filter, + ExpressionValueFilter, Arguments, Promise >; @@ -94,11 +94,10 @@ export function timelionFunctionFactory(initialize: InitializeArguments): () => fn: (input, args): Promise => { // Timelion requires a time range. Use the time range from the timefilter element in the // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = input.and.find(and => and.type === 'time'); + const timeFilter = input.and.find(and => and.filterType === 'time'); const range = timeFilter ? { min: timeFilter.from, max: timeFilter.to } : parseDateMath({ from: args.from, to: args.to }, args.timezone, initialize.timefilter); - const body = { extended: { es: { diff --git a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts index b422a9451293f..77be181d47378 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts @@ -5,19 +5,21 @@ */ import { buildEmbeddableFilters } from './build_embeddable_filters'; -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; -const columnFilter: Filter = { +const columnFilter: ExpressionValueFilter = { + type: 'filter', and: [], value: 'filter-value', column: 'filter-column', - type: 'exactly', + filterType: 'exactly', }; -const timeFilter: Filter = { +const timeFilter: ExpressionValueFilter = { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts index 1a5d2119a94b6..aa915d0d3d02a 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; // @ts-ignore Untyped Local import { buildBoolArray } from './build_bool_array'; import { @@ -20,9 +20,9 @@ export interface EmbeddableFilterInput { const TimeFilterType = 'time'; -function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { +function getTimeRangeFromFilters(filters: ExpressionValueFilter[]): TimeRange | undefined { const timeFilter = filters.find( - filter => filter.type !== undefined && filter.type === TimeFilterType + filter => filter.filterType !== undefined && filter.filterType === TimeFilterType ); return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined @@ -33,11 +33,12 @@ function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { : undefined; } -export function getQueryFilters(filters: Filter[]): DataFilter[] { - return buildBoolArray(filters).map(esFilters.buildQueryFilter); +export function getQueryFilters(filters: ExpressionValueFilter[]): DataFilter[] { + const dataFilters = filters.map(filter => ({ ...filter, type: filter.filterType })); + return buildBoolArray(dataFilters).map(esFilters.buildQueryFilter); } -export function buildEmbeddableFilters(filters: Filter[]): EmbeddableFilterInput { +export function buildEmbeddableFilters(filters: ExpressionValueFilter[]): EmbeddableFilterInput { return { timeRange: getTimeRangeFromFilters(filters), filters: getQueryFilters(filters), diff --git a/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js b/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js index e8a4d704118e8..7c025ed8dee9b 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js @@ -14,13 +14,13 @@ import * as filters from './filters'; export function getESFilter(filter) { - if (!filters[filter.type]) { - throw new Error(`Unknown filter type: ${filter.type}`); + if (!filters[filter.filterType]) { + throw new Error(`Unknown filter type: ${filter.filterType}`); } try { - return filters[filter.type](filter); + return filters[filter.filterType](filter); } catch (e) { - throw new Error(`Could not create elasticsearch filter from ${filter.type}`); + throw new Error(`Could not create elasticsearch filter from ${filter.filterType}`); } } diff --git a/x-pack/legacy/plugins/canvas/types/state.ts b/x-pack/legacy/plugins/canvas/types/state.ts index 13c8f7a9176ab..e9b580f81e668 100644 --- a/x-pack/legacy/plugins/canvas/types/state.ts +++ b/x-pack/legacy/plugins/canvas/types/state.ts @@ -6,7 +6,7 @@ import { Datatable, - Filter, + ExpressionValueFilter, ExpressionImage, ExpressionFunction, KibanaContext, @@ -46,7 +46,7 @@ interface ElementStatsType { type ExpressionType = | Datatable - | Filter + | ExpressionValueFilter | ExpressionImage | KibanaContext | KibanaDatatable From 306a5fe55ebbf05d095e5f0504e495cef2032049 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 4 May 2020 10:53:06 -0600 Subject: [PATCH 006/143] Use brotli compression for some KP assets (#64367) --- package.json | 2 + packages/kbn-optimizer/package.json | 2 + .../basic_optimization.test.ts | 39 +++++++--- .../src/worker/webpack.config.ts | 11 +++ packages/kbn-ui-shared-deps/package.json | 1 + packages/kbn-ui-shared-deps/webpack.config.js | 11 +++ renovate.json5 | 2 + src/dev/renovate/package_groups.ts | 12 ++- .../bundles_route/dynamic_asset_response.ts | 45 +++++++++++- test/functional/apps/bundles/index.js | 73 +++++++++++++++++++ test/functional/config.js | 3 +- test/functional/services/index.ts | 2 + test/functional/services/supertest.ts | 29 ++++++++ typings/accept.d.ts | 23 ++++++ yarn.lock | 42 ++++++++--- 15 files changed, 274 insertions(+), 23 deletions(-) create mode 100644 test/functional/apps/bundles/index.js create mode 100644 test/functional/services/supertest.ts create mode 100644 typings/accept.d.ts diff --git a/package.json b/package.json index 1e3ddc976aa67..e711235e16ea5 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "@types/tar": "^4.0.3", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", + "accept": "3.0.2", "angular": "^1.7.9", "angular-aria": "^1.7.9", "angular-elastic": "^2.5.1", @@ -310,6 +311,7 @@ "@percy/agent": "^0.26.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", + "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", "@types/babel__core": "^7.1.2", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b3e5a8c518682..b7c9a63897bf9 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/compression-webpack-plugin": "^2.0.1", "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", @@ -23,6 +24,7 @@ "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", + "compression-webpack-plugin": "^3.1.0", "cpy": "^8.0.0", "css-loader": "^3.4.2", "del": "^5.1.0", diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index ad743933e1171..248b0b7cf4c97 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Fs from 'fs'; +import Zlib from 'zlib'; import { inspect } from 'util'; import cpy from 'cpy'; @@ -124,17 +125,12 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { ); assert('produce zero unexpected states', otherStates.length === 0, otherStates); - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') - ).toMatchSnapshot('foo bundle'); - - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/1.plugin.js'), 'utf8') - ).toMatchSnapshot('1 async bundle'); - - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') - ).toMatchSnapshot('bar bundle'); + expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); + expectFileMatchesSnapshotWithCompression( + 'plugins/foo/target/public/1.plugin.js', + '1 async bundle' + ); + expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); const foo = config.bundles.find(b => b.id === 'foo')!; expect(foo).toBeTruthy(); @@ -203,3 +199,24 @@ it('uses cache on second run and exist cleanly', async () => { ] `); }); + +/** + * Verifies that the file matches the expected output and has matching compressed variants. + */ +const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabel: string) => { + const raw = Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, filePath), 'utf8'); + expect(raw).toMatchSnapshot(snapshotLabel); + + // Verify the brotli variant matches + expect( + // @ts-ignore @types/node is missing the brotli functions + Zlib.brotliDecompressSync( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`)) + ).toString() + ).toEqual(raw); + + // Verify the gzip variant matches + expect( + Zlib.gunzipSync(Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.gz`))).toString() + ).toEqual(raw); +}; diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index cc3fa8c2720de..95e826e7620aa 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -28,6 +28,7 @@ import TerserPlugin from 'terser-webpack-plugin'; import webpackMerge from 'webpack-merge'; // @ts-ignore import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; @@ -319,6 +320,16 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { IS_KIBANA_DISTRIBUTABLE: `"true"`, }, }), + new CompressionPlugin({ + algorithm: 'brotliCompress', + filename: '[path].br', + test: /\.(js|css)$/, + }), + new CompressionPlugin({ + algorithm: 'gzip', + filename: '[path].gz', + test: /\.(js|css)$/, + }), ], optimization: { diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index a60e2b0449d95..ec61e8519c960 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -14,6 +14,7 @@ "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", + "compression-webpack-plugin": "^3.1.0", "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index bf63c57765859..52e7bb620b50b 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -20,6 +20,7 @@ const Path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); const { REPO_ROOT } = require('@kbn/dev-utils'); const webpack = require('webpack'); @@ -117,5 +118,15 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ new webpack.DefinePlugin({ 'process.env.NODE_ENV': dev ? '"development"' : '"production"', }), + new CompressionPlugin({ + algorithm: 'brotliCompress', + filename: '[path].br', + test: /\.(js|css)$/, + }), + new CompressionPlugin({ + algorithm: 'gzip', + filename: '[path].gz', + test: /\.(js|css)$/, + }), ], }); diff --git a/renovate.json5 b/renovate.json5 index c0ddcaf4f23c8..61b2485ecf44b 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -398,6 +398,8 @@ '@types/good-squeeze', 'inert', '@types/inert', + 'accept', + '@types/accept', ], }, { diff --git a/src/dev/renovate/package_groups.ts b/src/dev/renovate/package_groups.ts index 1bc65fd149f47..9f5aa8556ac21 100644 --- a/src/dev/renovate/package_groups.ts +++ b/src/dev/renovate/package_groups.ts @@ -159,7 +159,17 @@ export const RENOVATE_PACKAGE_GROUPS: PackageGroup[] = [ { name: 'hapi', packageWords: ['hapi'], - packageNames: ['hapi', 'joi', 'boom', 'hoek', 'h2o2', '@elastic/good', 'good-squeeze', 'inert'], + packageNames: [ + 'hapi', + 'joi', + 'boom', + 'hoek', + 'h2o2', + '@elastic/good', + 'good-squeeze', + 'inert', + 'accept', + ], }, { diff --git a/src/optimize/bundles_route/dynamic_asset_response.ts b/src/optimize/bundles_route/dynamic_asset_response.ts index a020c6935eeec..2f5395341abb1 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.ts +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -21,6 +21,7 @@ import Fs from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; +import Accept from 'accept'; import Boom from 'boom'; import Hapi from 'hapi'; @@ -37,6 +38,41 @@ const asyncOpen = promisify(Fs.open); const asyncClose = promisify(Fs.close); const asyncFstat = promisify(Fs.fstat); +async function tryToOpenFile(filePath: string) { + try { + return await asyncOpen(filePath, 'r'); + } catch (e) { + if (e.code === 'ENOENT') { + return undefined; + } else { + throw e; + } + } +} + +async function selectCompressedFile(acceptEncodingHeader: string | undefined, path: string) { + let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; + + const supportedEncodings = Accept.encodings(acceptEncodingHeader, ['br', 'gzip']); + + if (supportedEncodings[0] === 'br') { + fileEncoding = 'br'; + fd = await tryToOpenFile(`${path}.br`); + } + if (!fd && supportedEncodings.includes('gzip')) { + fileEncoding = 'gzip'; + fd = await tryToOpenFile(`${path}.gz`); + } + if (!fd) { + fileEncoding = undefined; + // Use raw open to trigger exception if it does not exist + fd = await asyncOpen(path, 'r'); + } + + return { fd, fileEncoding }; +} + /** * Create a Hapi response for the requested path. This is designed * to replicate a subset of the features provided by Hapi's Inert @@ -74,6 +110,7 @@ export async function createDynamicAssetResponse({ isDist: boolean; }) { let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; try { const path = resolve(bundlesPath, request.params.path); @@ -86,7 +123,7 @@ export async function createDynamicAssetResponse({ // we use and manage a file descriptor mostly because // that's what Inert does, and since we are accessing // the file 2 or 3 times per request it seems logical - fd = await asyncOpen(path, 'r'); + ({ fd, fileEncoding } = await selectCompressedFile(request.headers['accept-encoding'], path)); const stat = await asyncFstat(fd); const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); @@ -113,6 +150,12 @@ export async function createDynamicAssetResponse({ response.header('cache-control', 'must-revalidate'); } + // If we manually selected a compressed file, specify the encoding header. + // Otherwise, let Hapi automatically gzip the response. + if (fileEncoding) { + response.header('content-encoding', fileEncoding); + } + return response; } catch (error) { if (fd) { diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js new file mode 100644 index 0000000000000..8a25c7cd1fafc --- /dev/null +++ b/test/functional/apps/bundles/index.js @@ -0,0 +1,73 @@ +/* + * 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. + */ + +/** + * These supertest-based tests live in the functional test suite because they depend on the optimizer bundles being built + * and served + */ +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('bundle compression', function() { + this.tags('ciGroup12'); + + let buildNum; + before(async () => { + const resp = await supertest.get('/api/status').expect(200); + buildNum = resp.body.version.build_number; + }); + + it('returns gzip files when client only supports gzip', () => + supertest + // We use the kbn-ui-shared-deps for these tests since they are always built with br compressed outputs, + // even in dev. Bundles built by @kbn/optimizer are only built with br compression in dist mode. + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip') + .expect(200) + .expect('Content-Encoding', 'gzip')); + + it('returns br files when client only supports br', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'br') + .expect(200) + .expect('Content-Encoding', 'br')); + + it('returns br files when client only supports gzip and br', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip, br') + .expect(200) + .expect('Content-Encoding', 'br')); + + it('returns gzip files when client prefers gzip', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip;q=1.0, br;q=0.5') + .expect(200) + .expect('Content-Encoding', 'gzip')); + + it('returns gzip files when no brotli version exists', () => + supertest + .get(`/${buildNum}/bundles/commons.style.css`) // legacy optimizer does not create brotli outputs + .set('Accept-Encoding', 'gzip, br') + .expect(200) + .expect('Content-Encoding', 'gzip')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.js index 0fbde95afe12c..8cc0a34e352a9 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -25,11 +25,12 @@ export default async function({ readConfigFile }) { return { testFiles: [ + require.resolve('./apps/bundles'), require.resolve('./apps/console'), - require.resolve('./apps/getting_started'), require.resolve('./apps/context'), require.resolve('./apps/dashboard'), require.resolve('./apps/discover'), + require.resolve('./apps/getting_started'), require.resolve('./apps/home'), require.resolve('./apps/management'), require.resolve('./apps/saved_objects_management'), diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index a10bb013b3af4..02ed9e9865d9a 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -51,6 +51,7 @@ import { ToastsProvider } from './toasts'; import { PieChartProvider } from './visualizations'; import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; +import { KibanaSupertestProvider } from './supertest'; export const services = { ...commonServiceProviders, @@ -83,4 +84,5 @@ export const services = { toasts: ToastsProvider, savedQueryManagementComponent: SavedQueryManagementComponentProvider, elasticChart: ElasticChartProvider, + supertest: KibanaSupertestProvider, }; diff --git a/test/functional/services/supertest.ts b/test/functional/services/supertest.ts new file mode 100644 index 0000000000000..30c7db87a8f8b --- /dev/null +++ b/test/functional/services/supertest.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; +import { format as formatUrl } from 'url'; + +import supertestAsPromised from 'supertest-as-promised'; + +export function KibanaSupertestProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + return supertestAsPromised(kibanaServerUrl); +} diff --git a/typings/accept.d.ts b/typings/accept.d.ts new file mode 100644 index 0000000000000..69cadc7491eeb --- /dev/null +++ b/typings/accept.d.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module 'accept' { + // @types/accept does not include the `preferences` argument so we override the type to include it + export function encodings(encodingHeader?: string, preferences?: string[]): string[]; +} diff --git a/yarn.lock b/yarn.lock index 3c233b76f1a48..941143e76483e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3624,6 +3624,11 @@ dependencies: "@turf/helpers" "6.x" +"@types/accept@3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/accept/-/accept-3.1.1.tgz#74457f6afabd23181e32b6bafae238bda0ce0da7" + integrity sha512-pXwi0bKUriKuNUv7d1xwbxKTqyTIzmMr1StxcGARmiuTLQyjNo+YwDq0w8dzY8wQjPofdgs1hvQLTuJaGuSKiQ== + "@types/angular-mocks@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@types/angular-mocks/-/angular-mocks-1.7.0.tgz#310d999a3c47c10ecd8eef466b5861df84799429" @@ -3852,6 +3857,13 @@ dependencies: "@types/color-convert" "*" +"@types/compression-webpack-plugin@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/compression-webpack-plugin/-/compression-webpack-plugin-2.0.1.tgz#4db78c398c8e973077cc530014d6513f1c693951" + integrity sha512-40oKg2aByfUPShpYBkldYwOcO34yaqOIPdlUlR1+F3MFl2WfpqYq2LFKOcgjU70d1r1L8r99XHkxYdhkGajHSw== + dependencies: + "@types/webpack" "*" + "@types/cookiejar@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" @@ -5454,7 +5466,7 @@ abortcontroller-polyfill@^1.4.0: resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== -accept@3.x.x: +accept@3.0.2, accept@3.x.x: version "3.0.2" resolved "https://registry.yarnpkg.com/accept/-/accept-3.0.2.tgz#83e41cec7e1149f3fd474880423873db6c6cc9ac" integrity sha512-bghLXFkCOsC1Y2TZ51etWfKDs6q249SAoHTZVfzWWdlZxoij+mgkj9AmUJWQpDY48TfnrTDIe43Xem4zdMe7mQ== @@ -9501,6 +9513,18 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.40.0 < 2" +compression-webpack-plugin@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz#9f510172a7b5fae5aad3b670652e8bd7997aeeca" + integrity sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug== + dependencies: + cacache "^13.0.1" + find-cache-dir "^3.0.0" + neo-async "^2.5.0" + schema-utils "^2.6.1" + serialize-javascript "^2.1.2" + webpack-sources "^1.0.1" + compression@^1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" @@ -31654,18 +31678,18 @@ webpack-merge@4.2.2, webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" -webpack-sources@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" - integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== +webpack-sources@^1.0.1, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== +webpack-sources@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== dependencies: source-list-map "^2.0.0" source-map "~0.6.1" From 6ab1b20eeb5587d66535ff9c97f54370b67ea0a1 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 4 May 2020 10:53:47 -0600 Subject: [PATCH 007/143] Display global loading bar while applications are mounting (#64556) --- .../application_service.test.ts.snap | 1 + .../application/application_service.test.ts | 77 +++++++++++++++- .../application/application_service.tsx | 4 + .../integration_tests/router.test.tsx | 30 ++++--- .../application/integration_tests/utils.tsx | 5 +- .../public/application/ui/app_container.scss | 25 ++++++ .../application/ui/app_container.test.tsx | 90 ++++++++++++++++++- .../public/application/ui/app_container.tsx | 35 ++++++-- src/core/public/application/ui/app_router.tsx | 6 +- 9 files changed, 242 insertions(+), 31 deletions(-) create mode 100644 src/core/public/application/ui/app_container.scss diff --git a/src/core/public/application/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap index 376b320b64ea9..c085fb028cd5a 100644 --- a/src/core/public/application/__snapshots__/application_service.test.ts.snap +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -80,5 +80,6 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = ` } mounters={Map {}} setAppLeaveHandler={[Function]} + setIsMounting={[Function]} /> `; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index e29837aecb125..04ff844ffc150 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -20,7 +20,7 @@ import { createElement } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; @@ -30,6 +30,7 @@ import { MockCapabilitiesService, MockHistory } from './application_service.test import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types'; +import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { return { @@ -452,9 +453,9 @@ describe('#setup()', () => { const container = setupDeps.context.createContextContainer.mock.results[0].value; const pluginId = Symbol(); - const mount = () => () => undefined; - registerMountContext(pluginId, 'test' as any, mount); - expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount); + const appMount = () => () => undefined; + registerMountContext(pluginId, 'test' as any, appMount); + expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', appMount); }); }); @@ -809,6 +810,74 @@ describe('#start()', () => { `); }); + it('updates httpLoadingCount$ while mounting', async () => { + // Use a memory history so that mounting the component will work + const { createMemoryHistory } = jest.requireActual('history'); + const history = createMemoryHistory(); + setupDeps.history = history; + + const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + // Create an app and a promise that allows us to control when the app completes mounting + const createWaitingApp = (props: Partial): [App, () => void] => { + let finishMount: () => void; + const mountPromise = new Promise(resolve => (finishMount = resolve)); + const app = { + id: 'some-id', + title: 'some-title', + mount: async () => { + await mountPromise; + return () => undefined; + }, + ...props, + }; + + return [app, finishMount!]; + }; + + // Create some dummy applications + const { register } = service.setup(setupDeps); + const [alphaApp, finishAlphaMount] = createWaitingApp({ id: 'alpha' }); + const [betaApp, finishBetaMount] = createWaitingApp({ id: 'beta' }); + register(Symbol(), alphaApp); + register(Symbol(), betaApp); + + const { navigateToApp, getComponent } = await service.start(startDeps); + const httpLoadingCount$ = startDeps.http.addLoadingCountSource.mock.calls[0][0]; + const stop$ = new Subject(); + const currentLoadingCount$ = new BehaviorSubject(0); + httpLoadingCount$.pipe(takeUntil(stop$)).subscribe(currentLoadingCount$); + const loadingPromise = httpLoadingCount$.pipe(bufferCount(5), takeUntil(stop$)).toPromise(); + mount(getComponent()!); + + await act(() => navigateToApp('alpha')); + expect(currentLoadingCount$.value).toEqual(1); + await act(async () => { + finishAlphaMount(); + await flushPromises(); + }); + expect(currentLoadingCount$.value).toEqual(0); + + await act(() => navigateToApp('beta')); + expect(currentLoadingCount$.value).toEqual(1); + await act(async () => { + finishBetaMount(); + await flushPromises(); + }); + expect(currentLoadingCount$.value).toEqual(0); + + stop$.next(); + const loadingCounts = await loadingPromise; + expect(loadingCounts).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 0, + 1, + 0, + ] + `); + }); + it('sets window.location.href when navigating to legacy apps', async () => { setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' }); setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index bafa1932e5e92..0dd77072e9eaf 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -238,6 +238,9 @@ export class ApplicationService { throw new Error('ApplicationService#setup() must be invoked before start.'); } + const httpLoadingCount$ = new BehaviorSubject(0); + http.addLoadingCountSource(httpLoadingCount$); + this.registrationClosed = true; window.addEventListener('beforeunload', this.onBeforeUnload); @@ -303,6 +306,7 @@ export class ApplicationService { mounters={availableMounters} appStatuses$={applicationStatuses$} setAppLeaveHandler={this.setAppLeaveHandler} + setIsMounting={isMounting => httpLoadingCount$.next(isMounting ? 1 : 0)} /> ); }, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 2f26bc1409104..915c58b28ad6d 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -40,7 +40,7 @@ describe('AppContainer', () => { }; const mockMountersToMounters = () => new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); - const setAppLeaveHandlerMock = () => undefined; + const noop = () => undefined; const mountersToAppStatus$ = () => { return new BehaviorSubject( @@ -86,7 +86,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={appStatuses$} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); }); @@ -98,7 +99,7 @@ describe('AppContainer', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -110,7 +111,7 @@ describe('AppContainer', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -124,7 +125,7 @@ describe('AppContainer', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -136,7 +137,7 @@ describe('AppContainer', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -148,7 +149,7 @@ describe('AppContainer', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -162,7 +163,7 @@ describe('AppContainer', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -174,7 +175,7 @@ describe('AppContainer', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -186,7 +187,7 @@ describe('AppContainer', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -214,7 +215,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); @@ -245,7 +247,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); @@ -286,7 +289,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 9092177da5ad4..fa04b56f83ba1 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -18,6 +18,7 @@ */ import React, { ReactElement } from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; @@ -34,7 +35,9 @@ export const createRenderer = (element: ReactElement | null): Renderer => { return () => new Promise(async resolve => { if (dom) { - dom.update(); + await act(async () => { + dom.update(); + }); } setImmediate(() => resolve(dom)); // flushes any pending promises }); diff --git a/src/core/public/application/ui/app_container.scss b/src/core/public/application/ui/app_container.scss new file mode 100644 index 0000000000000..4f8fec10a97e1 --- /dev/null +++ b/src/core/public/application/ui/app_container.scss @@ -0,0 +1,25 @@ +.appContainer__loading { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: $euiZLevel1; + animation-name: appContainerFadeIn; + animation-iteration-count: 1; + animation-timing-function: ease-in; + animation-duration: 2s; +} + +@keyframes appContainerFadeIn { + 0% { + opacity: 0; + } + + 50% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index c538227e8f098..2ee71a5bde7dc 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -18,6 +18,7 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { AppContainer } from './app_container'; @@ -28,6 +29,12 @@ import { ScopedHistory } from '../scoped_history'; describe('AppContainer', () => { const appId = 'someApp'; const setAppLeaveHandler = jest.fn(); + const setIsMounting = jest.fn(); + + beforeEach(() => { + setAppLeaveHandler.mockClear(); + setIsMounting.mockClear(); + }); const flushPromises = async () => { await new Promise(async resolve => { @@ -67,6 +74,7 @@ describe('AppContainer', () => { appStatus={AppStatus.inaccessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) @@ -86,10 +94,86 @@ describe('AppContainer', () => { expect(wrapper.text()).toEqual(''); - resolvePromise(); - await flushPromises(); - wrapper.update(); + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); expect(wrapper.text()).toContain('some-content'); }); + + it('should call setIsMounting while mounting', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = createMounter(waitPromise); + + const wrapper = mount( + + // Create a history using the appPath as the current location + new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) + } + /> + ); + + expect(setIsMounting).toHaveBeenCalledTimes(1); + expect(setIsMounting).toHaveBeenLastCalledWith(true); + + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); + + expect(setIsMounting).toHaveBeenCalledTimes(2); + expect(setIsMounting).toHaveBeenLastCalledWith(false); + }); + + it('should call setIsMounting(false) if mounting throws', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = { + appBasePath: '/base-path', + appRoute: '/some-route', + unmountBeforeMounting: false, + mount: async ({ element }: AppMountParameters) => { + await waitPromise; + throw new Error(`Mounting failed!`); + }, + }; + + const wrapper = mount( + + // Create a history using the appPath as the current location + new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) + } + /> + ); + + expect(setIsMounting).toHaveBeenCalledTimes(1); + expect(setIsMounting).toHaveBeenLastCalledWith(true); + + // await expect( + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); + // ).rejects.toThrow(); + + expect(setIsMounting).toHaveBeenCalledTimes(2); + expect(setIsMounting).toHaveBeenLastCalledWith(false); + }); }); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index e12a0f2cf2fcd..aad7e6dcf270a 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -26,9 +26,11 @@ import React, { MutableRefObject, } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; +import './app_container.scss'; interface Props { /** Path application is mounted on without the global basePath */ @@ -38,6 +40,7 @@ interface Props { appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; createScopedHistory: (appUrl: string) => ScopedHistory; + setIsMounting: (isMounting: boolean) => void; } export const AppContainer: FunctionComponent = ({ @@ -47,7 +50,9 @@ export const AppContainer: FunctionComponent = ({ setAppLeaveHandler, createScopedHistory, appStatus, + setIsMounting, }: Props) => { + const [showSpinner, setShowSpinner] = useState(true); const [appNotFound, setAppNotFound] = useState(false); const elementRef = useRef(null); const unmountRef: MutableRefObject = useRef(null); @@ -65,28 +70,42 @@ export const AppContainer: FunctionComponent = ({ } setAppNotFound(false); + setIsMounting(true); if (mounter.unmountBeforeMounting) { unmount(); } const mount = async () => { - unmountRef.current = - (await mounter.mount({ - appBasePath: mounter.appBasePath, - history: createScopedHistory(appPath), - element: elementRef.current!, - onAppLeave: handler => setAppLeaveHandler(appId, handler), - })) || null; + setShowSpinner(true); + try { + unmountRef.current = + (await mounter.mount({ + appBasePath: mounter.appBasePath, + history: createScopedHistory(appPath), + element: elementRef.current!, + onAppLeave: handler => setAppLeaveHandler(appId, handler), + })) || null; + } catch (e) { + // TODO: add error UI + } finally { + setShowSpinner(false); + setIsMounting(false); + } }; mount(); return unmount; - }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath]); + }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath, setIsMounting]); return ( {appNotFound && } + {showSpinner && ( +
+ +
+ )}
); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 4c135c5769067..ea7c5c9308fe2 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -32,6 +32,7 @@ interface Props { history: History; appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setIsMounting: (isMounting: boolean) => void; } interface Params { @@ -43,6 +44,7 @@ export const AppRouter: FunctionComponent = ({ mounters, setAppLeaveHandler, appStatuses$, + setIsMounting, }) => { const appStatuses = useObservable(appStatuses$, new Map()); const createScopedHistory = useMemo( @@ -67,7 +69,7 @@ export const AppRouter: FunctionComponent = ({ appPath={url} appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ appId, mounter, setAppLeaveHandler }} + {...{ appId, mounter, setAppLeaveHandler, setIsMounting }} /> )} />, @@ -92,7 +94,7 @@ export const AppRouter: FunctionComponent = ({ appId={id} appStatus={appStatuses.get(id) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ mounter, setAppLeaveHandler }} + {...{ mounter, setAppLeaveHandler, setIsMounting }} /> ); }} From 5e972e14d147e3c5232114690f61a96a493a28ae Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 4 May 2020 13:00:56 -0400 Subject: [PATCH 008/143] [Fleet] Better display of fleet requirements (#65027) --- .../ingest_manager/common/types/index.ts | 1 + .../common/types/rest_spec/fleet_setup.ts | 5 + x-pack/plugins/ingest_manager/kibana.json | 2 +- .../ingest_manager/hooks/use_fleet_status.tsx | 69 ++++++++++++ .../ingest_manager/hooks/use_request/index.ts | 1 + .../ingest_manager/hooks/use_request/setup.ts | 10 +- .../applications/ingest_manager/index.tsx | 5 +- .../agent_enrollment_flyout/index.tsx | 58 +++++++--- .../ingest_manager/sections/fleet/index.tsx | 18 ++- .../sections/fleet/setup_page/index.tsx | 103 ++++++++++++------ .../ingest_manager/types/index.ts | 2 + x-pack/plugins/ingest_manager/server/index.ts | 1 + .../plugins/ingest_manager/server/plugin.ts | 17 ++- .../server/routes/setup/handlers.ts | 47 +++++--- .../server/routes/setup/index.ts | 5 +- .../server/services/agent_config.ts | 10 +- .../server/services/app_context.ts | 27 ++++- .../server/services/datasource.ts | 3 + .../ingest_manager/server/services/output.ts | 5 +- .../ingest_manager/server/services/setup.ts | 7 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 22 files changed, 309 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 748bb14d2d35d..b357d0c2d75f4 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -14,6 +14,7 @@ export interface IngestManagerConfigType { }; fleet: { enabled: boolean; + tlsCheckDisabled: boolean; defaultOutputHost: string; kibana: { host?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index c4ba8ee595acf..ae4cb4e3fce49 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -7,3 +7,8 @@ export interface CreateFleetSetupResponse { isInitialized: boolean; } + +export interface GetFleetStatusResponse { + isReady: boolean; + missing_requirements: Array<'tls_required' | 'api_keys' | 'fleet_admin_user'>; +} diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index cef1a293c104b..382ea0444093d 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,5 +5,5 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features"] + "optionalPlugins": ["security", "features", "cloud"] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx new file mode 100644 index 0000000000000..ef40c171b9ca3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx @@ -0,0 +1,69 @@ +/* + * 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, useContext, useEffect } from 'react'; +import { useConfig } from './use_config'; +import { sendGetFleetStatus } from './use_request'; +import { GetFleetStatusResponse } from '../types'; + +interface FleetStatusState { + enabled: boolean; + isLoading: boolean; + isReady: boolean; + missingRequirements?: GetFleetStatusResponse['missing_requirements']; +} + +interface FleetStatus extends FleetStatusState { + refresh: () => Promise; +} + +const FleetStatusContext = React.createContext(undefined); + +export const FleetStatusProvider: React.FC = ({ children }) => { + const config = useConfig(); + const [state, setState] = useState({ + enabled: config.fleet.enabled, + isLoading: false, + isReady: false, + }); + async function sendGetStatus() { + try { + setState(s => ({ ...s, isLoading: true })); + const res = await sendGetFleetStatus(); + if (res.error) { + throw res.error; + } + + setState(s => ({ + ...s, + isLoading: false, + isReady: res.data?.isReady ?? false, + missingRequirements: res.data?.missing_requirements, + })); + } catch (error) { + setState(s => ({ ...s, isLoading: true })); + } + } + useEffect(() => { + sendGetStatus(); + }, []); + + return ( + sendGetStatus() }}> + {children} + + ); +}; + +export function useFleetStatus(): FleetStatus { + const context = useContext(FleetStatusContext); + + if (!context) { + throw new Error('FleetStatusContext not set'); + } + + return context; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index c39d2a5860bf0..25cdffc5c6651 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -12,3 +12,4 @@ export * from './enrollment_api_keys'; export * from './epm'; export * from './outputs'; export * from './settings'; +export * from './setup'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts index 04fdf9f66948f..e4e84e4701f13 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts @@ -5,7 +5,8 @@ */ import { sendRequest } from './use_request'; -import { setupRouteService } from '../../services'; +import { setupRouteService, fleetSetupRouteService } from '../../services'; +import { GetFleetStatusResponse } from '../../types'; export const sendSetup = () => { return sendRequest({ @@ -13,3 +14,10 @@ export const sendSetup = () => { method: 'post', }); }; + +export const sendGetFleetStatus = () => { + return sendRequest({ + path: fleetSetupRouteService.getFleetSetupPath(), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 295a35693726f..f0a0c90a18c24 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -23,6 +23,7 @@ import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; +import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; export interface ProtectedRouteProps extends RouteProps { @@ -142,7 +143,9 @@ const IngestManagerApp = ({ - + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx index dd34e7260b27b..e9347ccd2d6c9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx @@ -15,11 +15,15 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutFooter, + EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../../../types'; import { APIKeySelection } from './key_selection'; import { EnrollmentInstructions } from './instructions'; +import { useFleetStatus } from '../../../../../hooks/use_fleet_status'; +import { useLink } from '../../../../../hooks'; +import { FLEET_PATH } from '../../../../../constants'; interface Props { onClose: () => void; @@ -30,8 +34,11 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentConfigs = [], }) => { + const fleetStatus = useFleetStatus(); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + const fleetLink = useLink(FLEET_PATH); + return ( @@ -45,12 +52,33 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ - setSelectedAPIKeyId(keyId)} - /> - - + {fleetStatus.isReady ? ( + <> + setSelectedAPIKeyId(keyId)} + /> + + + + ) : ( + <> + + + + ), + }} + /> + + )} @@ -62,14 +90,16 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ /> - - - - - + {fleetStatus.isReady && ( + + + + + + )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index fac81ecc19cd1..b9c5418dbf6f3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -6,35 +6,31 @@ import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; import { Loading } from '../../components'; -import { useConfig, useCore, useRequest } from '../../hooks'; +import { useConfig, useCore } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; -import { fleetSetupRouteService } from '../../services'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; +import { useFleetStatus } from '../../hooks/use_fleet_status'; export const FleetApp: React.FunctionComponent = () => { const core = useCore(); const { fleet } = useConfig(); - const setupRequest = useRequest({ - method: 'get', - path: fleetSetupRouteService.getFleetSetupPath(), - }); + const fleetStatus = useFleetStatus(); if (!fleet.enabled) return null; - if (setupRequest.isLoading) { + if (fleetStatus.isLoading) { return ; } - if (setupRequest.data.isInitialized === false) { + if (fleetStatus.isReady === false) { return ( { - await setupRequest.sendRequest(); - }} + missingRequirements={fleetStatus.missingRequirements || []} + refresh={fleetStatus.refresh} /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index 96d4d01d67a49..4d89268c14b28 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -18,10 +18,12 @@ import { import { sendRequest, useCore } from '../../../hooks'; import { fleetSetupRouteService } from '../../../services'; import { WithoutHeaderLayout } from '../../../layouts'; +import { GetFleetStatusResponse } from '../../../types'; export const SetupPage: React.FunctionComponent<{ refresh: () => Promise; -}> = ({ refresh }) => { + missingRequirements: GetFleetStatusResponse['missing_requirements']; +}> = ({ refresh, missingRequirements }) => { const [isFormLoading, setIsFormLoading] = useState(false); const core = useCore(); @@ -40,46 +42,81 @@ export const SetupPage: React.FunctionComponent<{ } }; + const content = + missingRequirements.includes('tls_required') || missingRequirements.includes('api_keys') ? ( + <> + + + + +

+ +

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

+ +

+
+ + + + + + +
+ + + +
+
+ + + ); + return ( - + - - - - -

- -

-
- - - - - - -
- - - -
-
- + {content}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 602015d23cefb..ca5bf999aa81a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -20,6 +20,8 @@ export { DatasourceConfigRecordEntry, Output, DataStream, + // API schema - misc setup, status + GetFleetStatusResponse, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 951ff2337d8c7..6096af8d80801 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -26,6 +26,7 @@ export const config = { }), fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), + tlsCheckDisabled: schema.boolean({ defaultValue: false }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 3448685d1f279..3b0837565c36c 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, + HttpServerInfo, } from 'kibana/server'; import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { @@ -42,7 +43,6 @@ import { registerOutputRoutes, registerSettingsRoutes, } from './routes'; - import { IngestManagerConfigType } from '../common'; import { appContextService, @@ -52,12 +52,14 @@ import { AgentService, } from './services'; import { getAgentStatusById } from './services/agents'; +import { CloudSetup } from '../../cloud/server'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + cloud?: CloudSetup; } export type IngestManagerStartDeps = object; @@ -67,6 +69,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; + isProductionMode: boolean; + serverInfo?: HttpServerInfo; + cloud?: CloudSetup; } export type IngestManagerSetupContract = void; @@ -100,16 +105,23 @@ export class IngestManagerPlugin private licensing$!: Observable; private config$: Observable; private security: SecurityPluginSetup | undefined; + private cloud: CloudSetup | undefined; + + private isProductionMode: boolean; + private serverInfo: HttpServerInfo | undefined; constructor(private readonly initializerContext: PluginInitializerContext) { this.config$ = this.initializerContext.config.create(); + this.isProductionMode = this.initializerContext.env.mode.prod; } public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + this.serverInfo = core.http.getServerInfo(); this.licensing$ = deps.licensing.license$; if (deps.security) { this.security = deps.security; } + this.cloud = deps.cloud; registerSavedObjects(core.savedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -184,6 +196,9 @@ export class IngestManagerPlugin security: this.security, config$: this.config$, savedObjects: core.savedObjects, + isProductionMode: this.isProductionMode, + serverInfo: this.serverInfo, + cloud: this.cloud, }); licenseService.start(this.licensing$); return { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 837e73b966feb..542dfa9cefe8f 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -4,28 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ import { RequestHandler } from 'src/core/server'; -import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../../common'; +import { outputService, appContextService } from '../../services'; +import { GetFleetStatusResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; -export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { +export const getFleetStatusHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; - const successBody: CreateFleetSetupResponse = { isInitialized: true }; - const failureBody: CreateFleetSetupResponse = { isInitialized: false }; try { - const adminUser = await outputService.getAdminUser(soClient); - if (adminUser) { - return response.ok({ - body: successBody, - }); - } else { - return response.ok({ - body: failureBody, - }); + const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null; + const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled(); + const isTLSEnabled = appContextService.getServerInfo().protocol === 'https'; + const isProductionMode = appContextService.getIsProductionMode(); + const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; + const isTLSCheckDisabled = appContextService.getConfig()?.fleet?.tlsCheckDisabled ?? false; + + const missingRequirements: GetFleetStatusResponse['missing_requirements'] = []; + if (!isAdminUserSetup) { + missingRequirements.push('fleet_admin_user'); } - } catch (e) { + if (!isApiKeysEnabled) { + missingRequirements.push('api_keys'); + } + if (!isTLSCheckDisabled && !isCloud && isProductionMode && !isTLSEnabled) { + missingRequirements.push('tls_required'); + } + + const body: GetFleetStatusResponse = { + isReady: missingRequirements.length === 0, + missing_requirements: missingRequirements, + }; + return response.ok({ - body: failureBody, + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, }); } }; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index 5ee7ee7733220..43dcf47d26c18 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { IRouter } from 'src/core/server'; + import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import { IngestManagerConfigType } from '../../../common'; import { - getFleetSetupHandler, + getFleetStatusHandler, createFleetSetupHandler, ingestManagerSetupHandler, } from './handlers'; @@ -36,7 +37,7 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, - getFleetSetupHandler + getFleetStatusHandler ); // Create Fleet setup diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 5ecbaff8ad71e..84bcd7db3f7b1 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -314,10 +314,12 @@ class AgentConfigService { if (!config) { return null; } - const defaultOutput = await outputService.get( - soClient, - await outputService.getDefaultOutputId(soClient) - ); + + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + if (!defaultOutputId) { + throw new Error('Default output is not setup'); + } + const defaultOutput = await outputService.get(soClient, defaultOutputId); const agentConfig: FullAgentConfig = { id: config.id, diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index e917d2edd1309..5e538ad84b4c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -5,11 +5,12 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart } from 'src/core/server'; +import { SavedObjectsServiceStart, HttpServerInfo } from 'src/core/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { IngestManagerAppContext } from '../plugin'; +import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsPluginStart | undefined; @@ -17,11 +18,17 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; + private serverInfo: HttpServerInfo | undefined; + private isProductionMode: boolean = false; + private cloud?: CloudSetup; public async start(appContext: IngestManagerAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjects; this.security = appContext.security; this.savedObjects = appContext.savedObjects; + this.serverInfo = appContext.serverInfo; + this.isProductionMode = appContext.isProductionMode; + this.cloud = appContext.cloud; if (appContext.config$) { this.config$ = appContext.config$; @@ -41,9 +48,16 @@ class AppContextService { } public getSecurity() { + if (!this.security) { + throw new Error('Secury service not set.'); + } return this.security; } + public getCloud() { + return this.cloud; + } + public getConfig() { return this.configSubject$?.value; } @@ -58,6 +72,17 @@ class AppContextService { } return this.savedObjects; } + + public getIsProductionMode() { + return this.isProductionMode; + } + + public getServerInfo() { + if (!this.serverInfo) { + throw new Error('Server info not set.'); + } + return this.serverInfo; + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index affd9b2755881..0497bc5a2b541 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -196,6 +196,9 @@ class DatasourceService { outputService.getDefaultOutputId(soClient), ]); if (pkgInfo) { + if (!defaultOutputId) { + throw new Error('Default output is not set'); + } return packageToConfigDatasource(pkgInfo, '', defaultOutputId); } } diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index 395c9af4a4ca2..3628c5bd9e183 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -48,7 +48,7 @@ class OutputService { }); if (!outputs.saved_objects.length) { - throw new Error('No default output'); + return null; } return outputs.saved_objects[0].id; @@ -56,6 +56,9 @@ class OutputService { public async getAdminUser(soClient: SavedObjectsClientContract) { const defaultOutputId = await this.getDefaultOutputId(soClient); + if (!defaultOutputId) { + return null; + } const so = await appContextService .getEncryptedSavedObjects() ?.getDecryptedAsInternalUser(OUTPUT_SAVED_OBJECT_TYPE, defaultOutputId); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 390e240841611..3619628bd4f8b 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -109,7 +109,12 @@ export async function setupFleet( }); // save fleet admin user - await outputService.updateOutput(soClient, await outputService.getDefaultOutputId(soClient), { + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + if (!defaultOutputId) { + throw new Error('Default output does not exist'); + } + + await outputService.updateOutput(soClient, defaultOutputId, { fleet_enroll_username: FLEET_ENROLL_USERNAME, fleet_enroll_password: password, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index da8673da67f42..11d1f864a3619 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8370,9 +8370,7 @@ "xpack.ingestManager.noAccess.accessDeniedTitle": "アクセスが拒否されました", "xpack.ingestManager.overviewPageSubtitle": "Ingest Manager についてのロレムイプサム説明文。", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.setupPage.description": "フリートを使用するには、Elastic ユーザーを作成する必要があります。このユーザーは、API キーを作成して、logs-* および metrics-* に書き込むことができます。", "xpack.ingestManager.setupPage.enableFleet": "ユーザーを作成してフリートを有効にます", - "xpack.ingestManager.setupPage.title": "フリートを有効にする", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f66e9631b0168..9ee3b3d8f5931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8376,9 +8376,7 @@ "xpack.ingestManager.noAccess.accessDeniedTitle": "访问被拒绝", "xpack.ingestManager.overviewPageSubtitle": "Lorem ipsum some description about ingest manager.", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.setupPage.description": "要使用 Fleet,必须创建 Elastic 用户。此用户可以创建 API 密钥并写入到 logs-* and metrics-*。", "xpack.ingestManager.setupPage.enableFleet": "创建用户并启用 Fleet", - "xpack.ingestManager.setupPage.title": "启用 Fleet", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "取消", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", From 122450a4c889f50fc8a96ad30d849b7a260ad025 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 4 May 2020 18:04:42 +0100 Subject: [PATCH 009/143] chore(NA): skip functional test for visualize axis scalling preventing es snapshot promotion (#65100) --- test/functional/apps/visualize/_area_chart.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 8f2012d7f184d..05544029f62d7 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -242,7 +242,9 @@ export default function({ getService, getPageObjects }) { await inspector.close(); }); - it('does not scale top hit agg', async () => { + // Preventing ES Promotion for master (8.0) + // https://github.com/elastic/kibana/issues/64734 + it.skip('does not scale top hit agg', async () => { const expectedTableData = [ ['2015-09-20 00:00', '6', '9.035KB'], ['2015-09-20 01:00', '9', '5.854KB'], From 9db27dba56c245a118fd03ffa4e31475195ffbfe Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Mon, 4 May 2020 11:07:09 -0600 Subject: [PATCH 010/143] [SIEM] Remove forgotten rules that weren't deleted (#64974) * Remove stray rules that should've been deleted * Update rule.ts and tests * Remove deleted prebuilt rules from cypress ES archive (#1) --- x-pack/plugins/siem/cypress/objects/rule.ts | 2 +- .../rules/prepackaged_rules/index.ts | 39 ++++++------- .../windows_execution_via_regsvr32.json | 51 ----------------- ...windows_signed_binary_proxy_execution.json | 54 ------------------ ...uspicious_process_started_by_a_script.json | 54 ------------------ .../prebuilt_rules_loaded/data.json.gz | Bin 41865 -> 41851 bytes 6 files changed, 18 insertions(+), 182 deletions(-) delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts index ce920aeb957af..4e0189ea597da 100644 --- a/x-pack/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/plugins/siem/cypress/objects/rule.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const totalNumberOfPrebuiltRules = 130; +export const totalNumberOfPrebuiltRules = 127; interface Mitre { tactic: string; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts index c24f5bb64ef5e..9e185b5a5ef7c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -118,25 +118,23 @@ import rule108 from './windows_execution_msbuild_started_renamed.json'; import rule109 from './windows_execution_msbuild_started_unusal_process.json'; import rule110 from './windows_execution_via_compiled_html_file.json'; import rule111 from './windows_execution_via_net_com_assemblies.json'; -import rule112 from './windows_execution_via_regsvr32.json'; -import rule113 from './windows_execution_via_trusted_developer_utilities.json'; -import rule114 from './windows_html_help_executable_program_connecting_to_the_internet.json'; -import rule115 from './windows_injection_msbuild.json'; -import rule116 from './windows_misc_lolbin_connecting_to_the_internet.json'; -import rule117 from './windows_modification_of_boot_config.json'; -import rule118 from './windows_msxsl_network.json'; -import rule119 from './windows_net_command_system_account.json'; -import rule120 from './windows_persistence_via_application_shimming.json'; -import rule121 from './windows_priv_escalation_via_accessibility_features.json'; -import rule122 from './windows_process_discovery_via_tasklist_command.json'; -import rule123 from './windows_rare_user_runas_event.json'; -import rule124 from './windows_rare_user_type10_remote_login.json'; -import rule125 from './windows_register_server_program_connecting_to_the_internet.json'; -import rule126 from './windows_signed_binary_proxy_execution.json'; -import rule127 from './windows_suspicious_pdf_reader.json'; -import rule128 from './windows_suspicious_process_started_by_a_script.json'; -import rule129 from './windows_uac_bypass_event_viewer.json'; -import rule130 from './windows_whoami_command_activity.json'; +import rule112 from './windows_execution_via_trusted_developer_utilities.json'; +import rule113 from './windows_html_help_executable_program_connecting_to_the_internet.json'; +import rule114 from './windows_injection_msbuild.json'; +import rule115 from './windows_misc_lolbin_connecting_to_the_internet.json'; +import rule116 from './windows_modification_of_boot_config.json'; +import rule117 from './windows_msxsl_network.json'; +import rule118 from './windows_net_command_system_account.json'; +import rule119 from './windows_persistence_via_application_shimming.json'; +import rule120 from './windows_priv_escalation_via_accessibility_features.json'; +import rule121 from './windows_process_discovery_via_tasklist_command.json'; +import rule122 from './windows_rare_user_runas_event.json'; +import rule123 from './windows_rare_user_type10_remote_login.json'; +import rule124 from './windows_register_server_program_connecting_to_the_internet.json'; +import rule125 from './windows_suspicious_pdf_reader.json'; +import rule126 from './windows_uac_bypass_event_viewer.json'; +import rule127 from './windows_whoami_command_activity.json'; + export const rawRules = [ rule1, rule2, @@ -265,7 +263,4 @@ export const rawRules = [ rule125, rule126, rule127, - rule128, - rule129, - rule130, ]; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json deleted file mode 100644 index e8e7ddfc168dc..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "description": "Identifies scrobj.dll loaded into unusual Microsoft processes. This may indicate a malicious scriptlet is being executed in the target process.", - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Suspicious Script Object Execution", - "query": "event.code: 1 and scrobj.dll and (process.name:certutil.exe or process.name:regsvr32.exe or process.name:rundll32.exe)", - "risk_score": 21, - "rule_id": "b7333d08-be4b-4cb4-b81e-924ae37b3143", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json deleted file mode 100644 index be4ccef2a0887..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application whitelisting and signature validation.", - "false_positives": [ - "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." - ], - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Execution via Signed Binary", - "query": "event.code:1 and http and (process.name:certutil.exe or process.name:msiexec.exe)", - "risk_score": 21, - "rule_id": "7edb573f-1f9b-4161-8c19-c7c383bb17f2", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1218", - "name": "Signed Binary Proxy Execution", - "reference": "https://attack.mitre.org/techniques/T1218/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1218", - "name": "Signed Binary Proxy Execution", - "reference": "https://attack.mitre.org/techniques/T1218/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json deleted file mode 100644 index 235a04f8063fc..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "description": "Identifies a suspicious process being spawned from a script interpreter, which could be indicative of a potential phishing attack.", - "false_positives": [ - "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." - ], - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Suspicious Process spawning from Script Interpreter", - "query": "(process.parent.name:cmd.exe or process.parent.name:cscript.exe or process.parent.name:mshta.exe or process.parent.name:powershell.exe or process.parent.name:rundll32.exe or process.parent.name:wscript.exe or process.parent.name:wmiprvse.exe) and (process.name:bitsadmin.exe or process.name:certutil.exe or mshta.exe or process.name:nslookup.exe or process.name:schtasks.exe) and event.code:1", - "risk_score": 21, - "rule_id": "89db767d-99f9-479f-8052-9205fd3090c4", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/test/siem_cypress/es_archives/prebuilt_rules_loaded/data.json.gz b/x-pack/test/siem_cypress/es_archives/prebuilt_rules_loaded/data.json.gz index 573c006d1507d9c84c581c9abab7d60d96bfebe3..cac63ed9c585f993b805b80657b9888a40f7d819 100644 GIT binary patch literal 41851 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

-E8G)>IVgyF_DZXE!)%h|Xx9zPf(Xda{FHr+>K9@IBKd{bYp z^%j>4fP9`BJIck!51*~~hG-jYEJGDu$UCkHCf0;Nf5blC~ZG|lo;_~4(9_v7mmFx zyjZg{uE!Uf&(}fVi)fIz+xtsI!UJTH#>|&q0RPG*j$6h8DPk#OnP<+6#}|>8>>s`` z#F`gXk>^H@_%)kgZ2hCui2B{m(BE)4{LyVeF+QP_>Y#gQAveglu8L{hXx^U*YWJpA zYuYufP1X>TE6yYW;gji%(o(c4nPi_x$s};c{28_Kh3IadaRFuTP zW!i`IMFr)w$5z7$g^45ZrDdWps#V+1q_UrFnjH}-xfHXR$+M#JSZFtCbV#UG82nUf z=CGC2dP^B8Wi_N^ATSi>i6y$F{qR}fGeB_%qRm`TUQYF*U*Qo@>S!sp#v*sBG$ZbK zk%&T;Nl$?6EJ1emBf;FrkD?^xQJg(4pTr)iG!K$1z`w^|cX|8XnPv-Weg;a-=({cq zdM{t+zWkWEj*fG^{_X+D8Cz>H3EkW)V{E<;AB2n%4_tqH9j4?$dMIAVC{B@4IAf{n zfRq+P)&0h|m^VbL&J8K-xT@`JRMuII#2h$)SPlf%D4k8wZB z7@SVR;hD&s?1{CNhvN6pLsmb+_2IsVKH6w;h3wAgeJDYEdk+^k|0oXyF1p+MxyI6u zWC~bY%G_pt=*Eo8L^7EKP7sB8IOhU+xp2`B_YW?9Jiqwx4l@E|**J zp_g9HaNW;rt#XMW+daLNB3xV72A!vRpl_1y`mg6-7Q(@>JaUw6iJRYWI@H#bmsdTO zeoalWlHPi-RHqv7Rwp?&Ij||}%p|Io+K0B6cw1)|m?ucKFba+W-T);Q=nzZwV5SFn zbqIyr@LTPHyO>8B{{I#}0i!Zjv2oKCS`N~quTQa!o!%-pwo-Cho|U?Ju)A?9^nylPvW@-(k6;4-gsSHUN;+Ph(-bJdCpFQ3ApK zm<$dyIo-3%KYf%978>!;RK%Nqp@$+J1kUyjD{&ZkAYL(^Isx+okgDP+i&>n-PRtYT z<=lJm(n09#pLFos`8(^iPOnL19|C@y5 zHZ3Krwq-!K*OAAW6mN@o6yTiFM3+PdLTty}JgbtB;n!R#Qa-}B*o=0>F?ksvLt2}d znGPV_Naaf|$HWmwR2wMp5DO}l50JMu>Bf3ZK0!!Y5X{XR3HxP*4^MeBlm;IYwOKJ; z2mH}Z2K{*pQvEAMx~0PhnW*8fYvN#i7D09g4Kb(L9T3y;f-Z})T2Jc|D8p9q@O;X6 z<&RfqXU-q5-mFym6S6bknP9{5y)1RTkl9HPGC%iS#v?mqvE!#1=XRC{kE4LzqXZk@ zN5CKTr@Kp!nJ3T8oZO=i|DZN3>zw3h_6yHY5z~tj+nt^~IS@U$xBpmp^N;mVuyC|J z7Eb*z_a%pd{U~9+7fO&JoRkG&kZ>N_T%`adAf^rfoh-em^At6^!sabA7EZR7EbjERal9mp{2+z{Hss8A?2Pds6D*Hy*Na7nKc2s@ z%~894)UH2giDKiA^iU*<9nfb>A-ph3Z06#>{7A-(!|G)s38mz*94W^syemz*mA027#spX5uBY6spMmb&dP~8m zB}fWvD!)xTZviXrgtI$>0^2v3C`ZN8B8pq^?LkRFO*!7XSKvr-0&#S(2$qxUl_^Ir z9mNgOtr7I8!S2zL(Ct9i7fI`H3ZjYj$e%zhMW%SmJMVf^C^rQ2MRi@5G)&M`o!Cdi zS``S~!BgW}H3odR;8^sXB2B13&k=~e9u(-R+PZ@C7ev9ft+RrN8g)_KV9=s5Lxk%P zhHk8B;8kaP4LOQRKTT9xVlLjPyy5w|kba!7#K&a1fo(G$g*gUJIYE@=i5n(Id1GEx`K51@-&zhTIHL|%Rno3%XqV=TY(gtK?H0EiQhR%QW-BLd4Fd`+^QiO;JlQ}WUn+7iO*>qGl#Xs?~ zru;NBp+(~pZ5mxr|McSc$+6L7(lQe9gCx^?1djg;R_Jtj-`DrrsS=^!Q=DATWA~o+ z@>>C;y1ux)kR|N|I4YNFQZ-#yd4I`TKwVR=DB3%|&+ZZ0>3);>7yYrR{q6$gxxb`; zhY9-(r4L%kn{0GJv-xiqfB7;{qxc{8|0W*Jf!Z(E6n{ng*V=xqgJg23hJ0D}*Z0kK zug7xD6K4N6k1P+~17XO{*$pOR_OFb@dUZBDe_h3_DF+ii9i9E_0~Hb098t<@a8q0r z6DbN#h!TID{@yK;Ex8GN&$B(uQ{%^8!hAqXEOk?t#kR*&JG7%9eqwv+3j?)p&p$6V zU@gjB$Dz9MOdSZI_3U74K%0e`9fWbpq>mOyf&4~s=rIxGA`f|^-< zSWcrYe5-?jWMeR92h6H8H(_Yj8%xEjPe&M)+{cePT38!s#Ehs=dx@7$j6sdzKiUvS zs8D`GxS=gE9mKPKH$lhqHj~W{+eqNm5Hl&qym>%(PnoV9!<1n|fzK9>JW5Rk4TbfH z*J64^dJ>*c($l$_s<4Cz)5b@HLN9G^J9Ht09bAQK4-Pn=#_exWqN1 z$$xdHZr~fmHMB^F1(-X#LJUQ(nl}d4ul0>2wyt%ZTG!@2kFQ>aW0YwcvcJ*1_m}yG zvK)TY-4`;a6;Mq(Q5_`WVZ4u(!HA%21A^0VNFNS7ul7#MQ1@0+*^B#u^wwLrOy)ZL zhK-TiuqbQd;}X`1@%JS2=^Zs~Z^?5W1(R2b?1A5vdwMVXq_?tvt5bE_DCTE00bb%z zT+W4!y+qC{MLh&D-`v~`^l>%F>eJz@5E5YYDORh0!IFYfrl*rerZbo{HZB?L_U&La z9hVP7)OQB=Cr)m=x$UqdOmgNsVZ>q)Ma+(3H{zL}N%@p~z&{dB>s5AXPRzTIwex6X z?QE?t{`_h2_j&gP+>5w3pUFc(+)lJL;`ReCkcscH#LiOY=a~bNN@Qb7DJRLiC=WYv ziZGen^QjT%sNmFmEoK`5F}mHJ6o3^luQjIC&HwIdwBI`pDC??z|wHKp@K z+%^Z4FsAiXQ#EO&GjyjaYmSayT{k*MyH&+E`gQfR#cIlw203f$c@y*yZ;1R9AnOZL z290684Xg+FsJkYc3D00RP>zgU4`vg13s%$#bUB?3hj0oh?f7E)1x-Y@4$hHS*>-g) zXETAsuvH&C>nYN$?M8b}FST~oPK-ncvjWFmOKpQ9Y3ZU$u>DTUo-@$AQQyy{+74g^ zU1D!94cfnyS$jmBJ3^ekA8^liC9}O8j#^j1DH{eM{0t+Rxs?p|CphW(7+G$2qTaDmjb5Rm9OLoq3)R82SF$&iXCAG0SmG) zWIh);OR~sed1mKuet2H|q>P+L2(=pswfQt23WW0g?KO6hNCl5`mSva&D9Jp=V<%&o z>$s70lO*?@7Z0KKk<|Tjfz%vzKDGV~`=YQuR6|5L7g~pgrf2Iyg>z~h(&kQ|RmTs4 z$MEM1s!6#guCM;Yy5Vy@9<+hn>>I7&g4h1``J?sP0G`kD{5{z7W`=)~hUJab&Qhi1 z(}2c=3-)zuq-|?^_FNOjI@e%L_$}&pHYJ_GysHbtx-MAObVI<-U)Bh!G+#Oqz*Kd` zB)v?^%dXY?5fcdDqO6Ap5*4_RPDroP0YG&HcJ)nI@#tNt@(W45Ea@psFElPD&2=mB z_nH(#f4tJ4koAr}ed{%zJ!j^_QWxL+@#^U!4in)xHi)_`P}IZ;XK9kdk>}+$cOpCD z(t8wC*{g^{J6L(Mx8uFn8Gt{i48S`IJPV#U5XoJaQnS~qug$E*&GNn3MXcX38N;;L zVGtTLV*i8CZ1;PU|G`M!JDPU)IPLm>&mi*b@lwgwLdipgkj*#fLlHuv?e(}pkcUy| zyG(E)neVt6;j zWJfkhq6`>RGQ~o|r6nn1S_1@x)i_VGM2@y4T%2Ntq~f|L<#1y*%cEdHPaT3_u_PfV zAwbaxd{qmtGcss#VfB$+$P9P;B;J1{J&vVhe-aKIpuk`U>-zM)J$Gk#5vqO$VuJs@ zDL*$Gv-P3PczG9-!(-;%KhFCdcaL(yPUVEdWT^0x?K9BY36Ik#idd5PA@k!bW@#oR zOZ+Sgy(ms(oV<8BA@cW6PWW7lA~$3a0ObE%*HdH-%ZuTxS>B`{CK~iSh&9h;nh7b2 zDFHNq?*hO&K=l->%KB9i`?(PTXop{e|9Pto2`20bW1?sm#n35qgur;N28jf(MpY}v8A~+Ddi~s-P>xa+Cy+#a>I&UXYQ@536t1XiReTP{_vE7r=0ZeBnS*+S=K5g_zQQ1{ju<_s z$gB3mLSlmn#tL<=!98wnlb#3PKY~Ve^T&b4*9+JWlNxD9kmc)62MEy(dz~+depz{q z;LNMbW_kWJSCTtXq8kUHkAyg$FX1rqf|MnB8n8UhZQo9F9?8d%KkX4pT!*OUC{a^> z-0lb;uHvZODhA_w@Znp%1%9tzk5mlCM~wKRFyiK)<-uUYWNVDbvk>GN&taLH2*8Lg zXQ`h{CPT-SzHM{2^S*eY7!k(V-9x6Y9#!r6`(LczB*z>g??z}Lf2`Yb4=DC%P%|oX zPBs8I4Qp&vjeSyCle%R2jX4ok$!8Vr9RzE2(wR=TiKQ-Z)FNjRW)mqbW47Or0QZRb zy6ZnxQMlkj{5EUq--PEHTkvLe8j71J%$yra9OSLIci)0ov(Q_F;9z@ zT0j3eoFmoYL{YJs-xx251(@H(L@B+^?t?>zzTFi6l@g2D2!=S>XG&w1q9R z@W^e9iC6bc;xV|h)7Fp>@m0f}%{`}{^Q`EB6Y3%1bZ_8ONthXSa0hUTJ@rFwGnAi0bnL@CTe`x{4v~7X;C;8IAVZ%i? zC%V;|Z@25r!$@Sp@N8d5)yrE-K3iw-pQr3q`OzPqhE*HxMU;=Ml z`bymc>m9(cD*}Hj?-dy`<|;l{b&QW|+&lnhm9ueW*g;?myE2dAr#>HNG4dA3uDy-kYF3WB?gTov>UKcnRL>;kT#P!-saeJ&==?7{aU{42rvg zexA|S-gA*qYvN4zRozg3QxY|D+s2LUOfN%%-{_RPpxVDeD&z8!6qN`TFX=8tZ|cJ6 z!eu*4v9QY=I(NrQ+bbmRCiF<`*o!%5JPADJ=bmJ#2l9I;L>>jPFJvMg1tecm3NFXq z7GSK|8rLI?&FAYt5QZIZk1&EPh@CWykXlSIKk;0~^N2IY3nH0_BnzV#udIsfJycfx z6;E9_Q%4+Nxb?1{+;)!Wq_+BN+ma4w2hls%ZO=x8=XI+p&9G8oDuw@-`ja(L{9X8b zonBz#AlJ(d`@hBRAe&~+QB6YOKtDDcs9#49og>|zUxuc`NC{Hjg zsCN59J@W22)YCw6Kg}iMPUb#j&nKVq)SqkxCGo4C%S4)swn9PNvG=-Lt!ZRG!XHTy;$Y6DBRq-eD-rHQuY>uKpR3R`}g)Xh|x^HB1~bU-AjlXKCn3b3KO`f8<3 zj8fU;;N^b3l-hDB+#3NW>K7JLscvWu?bTq387~>xLhHIT$tEe~XIEVcts7PcsSbrYI|tS{E4AG@I7i7)ps(Rh&}?JodyC%4edO9td<^ zbQrBN^3KHNqMSAt{u?Dlnv`FL4dW6DHJMYdJMW5uSmc4me< zmsqY84@J|K!a@LrCa6Ger?9N9s2`G7>Y&3K+j<|cXOJ9!-E!Q;a9Uy=%NYure?>Ak3N^SL?@)aW`p zh~Rdjz;Ci;o5s78Yoh6Qs@SnB20s1We=eSp4w zJ%3}3kn0CC9&we49&i?7DeaqvSPj5Vfv5@#c9Pg`DKOmRa5q`)=_JKG@P({2Ko&Dp zJ66^$tlh4)&_V!d!mz_g)IutMee2IM<2^ifvf!n5QvkLl@Y*5g4z;QNAZe+=uTtSz zf@%xZ*Ym8Pl#i1z+rWC;nMcdp6B=CtvXhb*6`RU$(|I+Nb@jw5a*MXOOio~3DU66v zy`?#7a1pgiBgfkosuoZSmfOh&MRi@5gxnFqBb#YEOVON+9ckonR?Z`+FlN6jhzJ(f zui#J|)rgR~mxyaa%#Q8pDj!iEsa_6wlOfftPfYlUKTzoTHc{N3CNYHu)hQAYpkQRO zQ_Z~#v96w+fdbA@nqaf}<7#p#4_8+SJlnvq!3mH1K-;As)l35#EtRS6`*lWRF2ow0Z9*8WVw*?dmsuo4%vuQS2UPuGF@!GXt1ID~j!&Uq!YoJCIN1{__jkJ&h)U=PLh^K+Ef&&lH!Bq;UA*BQvhC1Uhd(fuh!buGqn zi(2*9^DjLqUCT-e;-ENz(PKJt4F3%iP6Qpxay@kgA1a6t0A+1$so>XjA+VEOx{eJ6 zL1ht9+lv<2kTUL{Qnla6zA6)k5dw{U1Vu5Dl*YAyJ%oBodw*Tl!$MPs@M~U_xC#b< zFwiNP#*9kI`55Zr4R*4uGZ4x;JKAD|ov|TRon%NryeFT1tx0P^GFhL5WFDJ`3Ino; z8D&FvNm-kellEi;B3J`0X;4Z%=?TGD5{)J_1F2Imn4c3lUZpe$D3EIT^k#%=m8#zq zBONvpE=ihKZe=CuOlU+@`jxItDF)*eRaOYxF162Bf9!EWs6Hdd>p?O?{83{3c@iVY zY~IreW5Trq&rbs61C5yPVBm@;a?BzX`@Zdm=_8XO9)+=^Ft-0V{3jaC9*V;^|3VK1 zhX?W2I6TUHC-uF6xk;LV5F-P|MdC8)!~n`gnE3L=6CX!Ow0}7Kcdc1B-d68>diD49 z7{SsPeDq%Q%`14iyv1B$EpiyZ{b2FfUvbYG^K69d#HHj7c|ZNVPUnfpdTM`zF#Ae> zp&CatV%oW0VJR`Alo|jX7y6%|g=+3S1s@fq5I-S5G2HH_#*WcS zB@=a)FEYN(sAQEQT-kbPfHbhYk*NWzYDE5qtu!e?$dL@$13T^tqN%|8$hxN+pa5>K z)c0R5%s3;^HpR`5i9_*7uM8tyn^hV>Zz$OO4SLZTYzScMIEer~d|Cn)* z8!!Pa&HA({hC{OAU~&}t)(3D9YxWF1b4s#FoBBr4Z__!8@=H6L5PF9Wfq*FBKfnL> zNsEcPyTT%-jTV-E3k?}qZ`J(YjB4w;Xwd}A$v{cO$|_GIPRX*BYE47cdcxtIo2pt} z6HFtHNMAOhRt;q($!tmu)%B5ZLO56LkgFA%nr`Ran)e86Qggi`l@OgcZ{^&UN3!qs z!iw`KX=SCzo-DF*Pwz3G^cM4Pb*heq31-J3AG=FjT$k$Oxl|*tm$J-CQTO?;8CNrX zv<$NPbT|Xi^i)=-Sgro0eFsh_jYRstDI1pzHsW?LnvTndxsBZ^^*E6X;~+>_>Y^)B z;>Q3B?3Cp(I@6?{%M|I*QMZ0}*tXoHSF)k8P~ zAFhht7!3DfOy{^45Dx~^!7z_gWD3jCeSL%vYW1WhP}?-I^M!Ufw#d)Cy$hKPP*0=P zq)WIX1yD96wKrxF1B8IOx7_E*8C+*0td|V~n}3 z`;x}z(jdG`nYBlZu_KJ(d!Fq^ZpxC#g_9}GLdLy}v)lVJ8vuvovL?A4wLuXdVw`>IUwMhZo)+W|TNbmhuaV z0|Y_*hIC->#^Qc_@g4Ng&m*Cd04yeluo|gdl}E)y(M}i|bu3j@WSS-g)4X|j`^Y+> zZvObtxqdE(C1U#(iU~cJPiM%q^UwiNMzYum9p*dmsl@?qHoohHVID}CK6gXrL9x@@ zL9(Wy){0@4eL__{g`(s${ufI6NAve-JCk<~#7A zVC7&3SXtPy$Pyo;VN>Qyk+6gZ0SnzQ6oCX3Eu0sRm3@EzuyU8b$oexTDY8CLelqJ~ z${Px8L#qA@0>zI`#`d;SiIXsJugizItJV*h6kiA}xE3khd(_1ei)@ll@r5stk%vWouHGY2bU)YHq zJ4-zlH(~Y; zx5>MDJRv{KU%!6-L=1u(L(jz`h~v+CJ;+AmHxw<>p|y_7Sy5p_*;A9wY>rT=j9+Cr zF}ZrJ<=nq8HJRjPPos^gtW^BeyI((319Y5rSzuUGF;x*R*9CNYOyNh;s7MC%3PvDl zCN@-!D^Q8;nzu&1f%9bOqpjDY5y6wAdyBgHleF7v7S^euzpM~v&SL~M8q|;IkHVy0 zc>rtMJ>+jshbmJykv}@9=?KH}Z#CO^ z)6E60#*1B+XLinm*!Pp{DJ7_5*xzC+mSg-L3cfa<#sdLgfn#saE8y57 z=K==LjyUr(!CCC^gmF)%PAsIGrQ*edFW22a@b$M*&BukYVm_YZb~QEi4V6r~GzE7= z(N0Q!t6;*3HH5Qwa-tnb^{J0cs(3`JgFn}<%N@Z{(dCx`xKh@`Avyz8CV7qGfYoZ= z%|OV#nf>_Gy5`L-VuTW%0u$oFH^kejLt4^3Zw(Z}em(!`Q=`=Ydx%PWKe4K&~8gW$6{U8P1<{#*RU_{^F zUP0%2X%u-*$ZWLt`)=a0)bVl_2cGYGi5ofY3lDfhj2P}8MqG86f7@fGC-kxnUYsa; z%zm!rxz0yxuXGDm95d(Cs2jV1ww>jDo)Yiq|EZVAo{?}*c)jEYI&QzU6k_&- z=JE$PdpXY?q8*+A_4&pGo&4y@KO|p?=R563eEukWzWMHaF!+4?%n2}Vo)Esng20QI zZ@Yq}p>SC!QUPE+la35uHa?FAVX%h=^mk~EL6~fC>j#8QNEN#|^F*J(O>?@Uel^1? zy7CE@p#s)wr+}B*d1RMLJS^&pkh0ns2_;uc5nb|{3pvj_W)dkk^)0G>0bvvwJfqTA zsjLQ@D|U|V8h7EvZ+X>%S0 z_DctTk+X*>v)+=MBkD`dsr(ChyAkuXbzVobu(xEA^855rVy=^@P7LI;+5!=x zwy{fO5y0=dBI1I1lS~!^a&$yCfjZ0RG}BiRUqX{9QLFL;MTPG?+>9I!O9CllUfuRO zqB^jl`uQ!UXreSyFxe$K?wcSmOwm>8EVt0kF5y%!z>Qnbpy>$P-C+Ky2DUNyfrFO= zrZ~_P1|=?_OKw*DrJmbM7`{uIjbkc<9r`qeew1giKj5&(mvCMEbwOdX}a2xyH{F|5N1JbiIvo4CA^fdizQhKUT%drm34QzytAv(unCW z4?qKWf=_A;567IKy)bhN-T9Cnz%*dH(*DY9>joG`oHiWcqAoTcmqGy zaH%TV0Vx)1cqkwlDQzy}fFUHnlJThm%?#l9^whPT=+tpeZSR!MD+Zzg`&GYk4ltED zHv6Q%8>T)>rasG-efsy$TSAuD$+95M;f%9Gm-z{IS;}Ld1zCo)cAk^@_7g&uAEKvQ z=We)e=G z2ZKqsSJX)!CTX4~%*}Jaq-hke1XFlQC(pdx$z_ zM7XOgg{BQFrM4@Vd`q#dKQQd)yKXcF6BdUHDgqcC?KKL;|btn`ovl`tyvD8y=8O=($bY|I=yXA z{|AiGs20p+j*~4xA*evN|DT>+n z0WkWAq#lZ-HlM3QK~moKwCCJTqZl9=bL~(tKTkZyQ_p7{Bpw(*k@DPo@knYPlP`Vz z?Va_SasyU!YW)VyxV7HF$^Qn?)wIq42pD)};)2c78^bMSh(xuXvOsFbH=TkU0fROU zBJxp|&^0ZC=Kl8X&2nP~7WRZ-j48yle=U&7M*3VLI?8Fot2Rfn6=f~$-Zi+3Tf@2* zSc)hFVH2V@8N=~O+%*k63s44t&kZUmNomNZd|t3(SP=@-@Xk298CJtCc9A?f#zw=f zj`=+&&%Z9thK|@SxCV`|y=+wYY(M%2ahZe{6z`0+P+d`7*X6Y)!#4$}57syvYC1;K zGJ;(_q;+R`INV~UEYb?L&2e`B{Xff$6FgN~Q(7cA+6Ad%Ejbwh07fVbgFcie3VYIe z!xkw+Hxk{$?86p;oIEgf0K9NJnJ2iv8A-(9U*3LYuvX?!5~h1nqNtL2W}~2u1gH6InoYD(VorccH(rNf7~!`VhDoG!)$03v z1V8FSX;$G_dw8jM<&RfqXU-q5wvQ;g8*_(#+ z#oRwab?rDSZ}v`go%xq$@tr!JcH?I1I8W?m>fg`Iso2$i$Nl!aZg%Kj>OTWER%e|X zZZDn;ciCpy8oTMJ{Ll%HzdzyqE?IxUbJ%3!LF2ChsXJT*@nKjlyq_m;YtrufCGL%P z>O+yZ{q1cRY2x{zpT&&(QNsKv$Qc(-#=J~=eh_6mOJ00*S+tLd#=gEgr*q4SZQI_V zaJ^9MN*1o3Scpaf5c95!rmn^$eS$)w01AVgB7- z3Atq#8S>y_PLey8?8%xw(2)Q2{7Xyv2wImWwS>|WVC@B&?xbIe+G!SVwVo zKg8LiB=aaqX7exfP$Zck+&pE7NO>?VOCt#V2oWP%oKTvDU%!4m*Y9X!5}Yp=e|h)s z7yYXP3l;=GX8^z_i(=DFQI;A_TIOp8@N01)8gwViDR@*BrO-LG0|7c26^%U-`9O*$ zn~jMEq?%_%!-LL5AefNN&c$rwxCR8=*q8$0?LD@nxj@iajk0X^An1QD@;|3GD7<|G z0*w(-$(ye2qA&0)tIK+LtE`q3;3}I5y2>w3gOOWIOBdUBUu!xG^L(C)jQs0#29j<_ zv|t4VP9-BZJ&+!9T#q1Y7&!~prUWqrvtD2%$MD{fR^y@8+D@&lcL-QNLvVUh%kBP& z-2GK?lo@}XJ=C#d8<(`_xFpQQ@cZT3MY*tLAftc@5hu)#;jdWOoQYgyPVB~Mkb94k zB9C^_qab+{B)e-{36h(Cq=zC%hVIsar0>8^4+F^j%y*bC+z3QU+hIt$6Zx4V)5t#x zlF9xFlE1w_w_g9IyeZ$y_G(&BI?Miwr{@kt$|cFM6D_;sfjUbL%9PkPBTKAEm3VW- z)P|8gDo@37n!j4RjFz{Z)2QKLBw?}7wNq0Z`HW(Q04fW3RjtgHgh*{7M$4Xku@lvL;A=7LUN_0`YAQj+xRiGC3d)m& z-}{xKIASD7s+lGf!?=w1EQtW>JrDuf6ZHf(eh6#khTozGCVoIz9E`=^XX8mHBIrKB z;Hm_e>-$2|l@TV8yg+J?;Z7h@YfhJjGl)_KEjbkTa8)xTEt*S3+DTcD41wR-grpr@ zaV<0l=Cb&w6m#pWcE{CEs}6uXi3Rp5=nvQx9LQCNjc1kVrisa-FN_?Ib3QK0g41m2 ziJ5a%SvBr>ojf%!N4I;wg0^0!RONYba_BPD-3g_FFiAY&GB`#v<~wk{Cb6GkV&o)E z6W`87@F*GTxme;p>b?IhZ6^mJJAF_mo^?J;%OYdHC})#iPL`vK>65ycuF|?5T}+Si zw7IsGJiYmbeK7L$_AaJ=%)`)?8FNz^!{7xeOF)ihX`01(6ojrXkCbik9@1m{^~*c! z^K{^Fz)V4@AT~FN%^l4?LMZjnPYB1>2u5*dDME_fAJxj|)G^LWf0MWXyLnhox}JvB-0! z;BFr9^l{;oM@QJBXloq>o9^0Hg3ab%>A?s#+taZ+so;6&yUgdVqsUrk)1$<+D{!FKxq;br4wlW)7Mj3!B&_4hi}vJQnqHHO91#Gd{B z+b0bWwZJoH5yeoev;nk9;c6Wj{8tq$MIcekC?s=H3^?gi=bsReh8h|EqeUGe%9nlb zjCt|D&@GPK-X|(?BneG&h%tZAA}9_J@))B9;mI2$$;bA=_(+{c{lr9!!5%@ueDE3w z98!yC`fVi-k+g$hVjVdNMk->qWBUIguZn+C3s2^fk;>pi$)c3a&5|Ikl^8cV9nA?= zU}ROO=u$I?ix$J>=z%_eEi^~~2&WuVI7gU^3Y&I|R|eMKuvdnOmI!QNWs6(G!gGSc z9JZ=Os@Fy>GLkeGn}kBmQj16~G$qekovn1u#L=}`r4@_1NwlV@Xv*W2l4Jo=k;Hk? z?a)j`TnBOXqWC~~;zmqT8XmzfQ*N$g=OB3F>UHUX1tn0 zDN-$rb-76`sHZJ3qniFo)UbCSssc7R$VSp6S-cYPADSepUr)49q(Ck7yoH7gthcnf zVN7c4x`6()#A$q^w+zHfG{yO5z?9$v?--StVQ*AWtkvprBQwEUap2K2pnCWG+z^X9$*~w~WPqf_!(=|~`bLb_XO-FS@R*J5xJ?yq27=DX@ zl0Yvk+!EHjNh3xgSEcP08X3=8EuGBSbI{hc4j4aY)OMpjjZ5`;!7N_t{$83jbt$tW zj@uQEOS$K}GId!Hdl~aRfB>oO3+CiPM496|QI;KX+)u-C`k=TM#BDx}hXUff?LnO6 zj+@#dVriD9FnC$Oxa&GB4Kgm1Sf+XGym%1jxcdjuRbza}V2TVCeF9>w~6rD1) z;(}VC-dsdSgfRfLU{XfdC~?eseA;!I&)UqWSdI5hDy`*rbQ-RH&ll>P%5PIEE$i%R zQ$X@gQ;%K2GAFvh9#>sUd0y{OaSc|R(aU*;?$Et#sJ)IN(9w`q6Tqo1Q0tApx4pC0 z!vZ?(^1?krn>%LrLD0sDVn^6Pz=A9cna@Se5)s-g&+MGTIq5~|<1&*Tq0NtkHk(i5 zp+Fmdd%ma){^$CR%OU~)@;v~DJd!C(-Ov`coxq8|bHLUiH-G7%0n4_4Lng`Z9z#3Pidsum`fT8a=PASjzxW>K2%Ib19uC)2l zg3ax!;~qthpYEeE59*sAzG;%-x42vYL-5q-X)Zo~_^ii(>|_)4=p<9BP&}#dR#D}c zER>qP0rYvC?`CMXj{px#ho!8<5a#>U!e)Qyd;pH)k0pw68IShZs2($h*Rl23Uxd%rf_c@@E)HH-LbdD8f&)2_26Rj**XYt5e*VI z*cx28xfBTi2^OT>#n_^RaX;fM44g2QJP(587+YlTAsMAHeyPxrA8Yzjbsi00N5j|A z@U@^1D)GZOn!SFEZYXeho{B7BB6R}h$AM%i2+AxEB0I`s*Uq}ISpDu%H}r(MAvzSk zuP#~_7{2i&9t3!c2Tr^_c#B|kTt9P|_Bx-NQB*k&UW?t_Wqr>pZ3blXS`*y;ot)a5!cvloxR$zve}WLvI4RO`yPL zs6>q{h7;(K(Lv~XXmq}z3KdMH34~WEhSUQ9oTk<_Z*I|0HaB3pOf?m{Rt6*3cG})RM=Jd7~Ge9_z@*-irVHFUb%DoFw<};YY1Bp@A z0DXYpyB7{~{euXoZVzB~BZEkgdD>25XP{<>mdQv=v6RT%$T-QY@hX5|nW`zF4 zy}VtW?gmiM1j*mv94#`+B(ji(In@Rp067ZAaIM|x=QER~q_o;ym1L<%`IOFHm=avA7*SI{oze;}+*o zi#)`)@(HN11?R5B@_WVDFYB- z9k)As)Av2>cpTPHZ`NpG9=1fcA)tHt%e>b*LsoG9*e6h z_BBVA<3oTi&yo}O%#SulFViBQP=7FH!;%drWuu`Sz}U3U2xoGPWA=0|^-dajEzDsR zY{#cCu&1N?6u?k*bNUO24u7AH|9%aFvDQCeTKyen?%%P%e}^>#k1Ocut~e=HksqZ2 zVT{8Ll0^|=zQ;%)<5USLBR7x_h?9D<+lPx>`np-mf5(*-cbe7j_n}IkN{9EMN{>xO zT9@#6^JV&2q@-|nmI|LU-;X^>`~+E#Ib@N_R1%p8k;X}&xTMcsN{SB9+d>Nt`w!Q$ zC?=T?0JWR^y0J%_IiwG_YeOT7KOH8jvaT1xx4c8=J4fO5xI%JaiwQLut>N2+af`gF zLzHtsKG5QsoX^I}AF9F=X)7;e)53a`nsaO|HgzJdFy4qy^AT2Vk(E~aYHTPj{qpLQ zlC3q}JwlTamv$GD3>oqjOdCXNdc5mYQD>}89i=j604<5}d|BFB;8XT6+vh;rTwKg4dNk^U!4;+MxB>Ew2U#h9gBaYmBBm4v!M zNO*>!yj>nEKgkj%XfOF4rXbKueupLbEr+!1CBLU2`8|P9H=8fh$0GS9yT|aPQ4sqq zVI=fIs+)|YQ5KObjaU=~%u|Vc_L5(Gi05PHKRYk9eJn_2X!-K8LD=6da3>aNPBxsE z{qXUgLU&G0433WH7%qUu0tf_@%m&=NH6&*~=?Ga8ox=5z9AsW|-$&M4-338V6w-M}r5GZWdIpaOqQN8?r!pOwcH-V|)UwHC|P zu~Ads)Mz|e5@}7P?}}PV_7}C(_y+6}wzQYAzu#G^-*(9Da`n~%@lu|f3++40)VD?t zRm_bOnz@8g855c*Mwm(>LOElZAG0X);;m%rK6+>`R)2S~+Fd1uwypEtIzxT1&QSa5 zbaz?O+;6A*s|ItYS~g_tx!vve*W>>3a6Y^qzjLO)v#8I6)7?{lKky{?(p9>lIaP|r zy3WTb2e3(g(w61Us~Ka{-bMfvrPrc7k``t2jrnM_D7#x3izt#VQSslCQ^Q%+Otq}yrDf^EZ^l+vuO)O{6J4SW)hDy2>%@{+>I3My zp={opMbl`-am#y%RKXHeS2;JH?=xTgTjI-FX&;hv* zha;t7bYa*?~3zT|iPaX;S3f$d&%o8_>DE9>k7)Q#T5YU&3 z6QW|DdQy6d$IqT_FmVqs##X}>-8h;5q(MOAqV#iFKSZRh4qtR-RqA)pMI*0FX;`1X z=IHQk>>O7zLf=kn+?<4*G@e@;NUi2w9yHBiL(XHj!$yK<3A((qFzi~>rVA(pt2K`y z8anDDHiQ+@oN5`8#91}r3k^ieqGf7+7>Z?d`L{gF3`1%dwjkz=05V+-(X|}_Aly|I zTp(V;(~BczVAfIMZ693ln+t69izZB0&%V4rOcY5<6P`vyq`r?KojBp4CkT(DIP(+> z;YW{&z8fZ5H!|1e-FPg3$lo0hWs=7(^AkcPSKRn(^5>>X>TQW>`Aj#(30@GN9!PAd>@;RYtH z(TRQ)>-}G*rvGKCf+UcrSQ|Jkiy<;Q8q*D|qa`j>H8KDg(!v@*WL0U_z;`B&hM9yd z8Fk^zp~?hYF);Lohfq$+?At=pd{>*my+#jcCIOvz|PCYY?0R7iOMV2B+cK z=O4@fIW<$^f7H62oEM0k063Y4cnbFN^ABrUU|8HW_w||%@3)$0NSX=}2N{W(7r_<+ zyGN1=Mbb>fo*&CN*^-!WZ4-@yIk$Tpzt>kEKg<_kC6Zd@^SW_Q9ScYbcDH<%F{eIt zq45$*2~B+p;8MoK&tw!tS?oud|Lj4McYs9AubMDMt){5aAHBxn%^M&*fYzu9%Vkb( zh&dx_{5mUW72Vla`M92O0=BrKVeXk@Nxn@H^r79ao3Q8Ra8FbE;ip=&Gl!A1r{_$6 zG*i34t9ctLqYYv)e_*FoOWjyszoIQS!Xq(7gC(QVY0A>o=@g48pEz&72{pvp{aIFq z$`HC(hvTqu*mL6r3~;m6PFZw#qT$fADo#v}?I%`4cU_p!?Xy^7+Q?Y9?7)HXq1(k6 zn!~3sDyzIiVD#&BGAzq4tqM$oT-%#XG-mOCadpI<~h9* zabqQ7qc5BVa%(h-VtM5|95DP8)rclEwP z<+PamuxOI|1N(XjH|Lf&v-+*No8wq^J2&U7XMa$m`VZ}o^`70p&|j6~{0BpP55fDo z?>NE+btHFVk^ zIijfiUwRkPtf19#)rX+H0ycmWa+`jN>c^a7&eu+(zn|8QNN z@viU3Z5rhps87pks&o7Ab6@4P77%}e9S^#G4f{;nkV_S9dc)Lc-R||=fgkO%Fm`0E z3+*|1(a~D6;aL_*A-L<3IEh^1d5R&!WkBLEO9PdOFp0dalnQs}yRe{+$M5ViUcjph zt|qv4;9~o0aSo_a>2+k_My?-r+h5y2{+hjpT)$v9`3sX5F#Bk{qK)G-pALLC+J5HV zSJn0U+LmLdv$Xa4+B=NCmg?(;j*fI+S9?+DMcqGlb$ifv`(n2zaT0Jpe0o#d3-0V{ zcg?P*XI?xA(jE5Z{dBc0$4<^0_jI+2eHKygK)QNSd%N7p-XMml9nAjtcXfEQW`{>- zVLV9U;CpxYLOHhSaAaW5FVD1}^EMr4zMttVp&zrq)>m5D;Ib zXpf~L!nvZv5Bz{9nebT{ZM760&1qROfEGHu)3V0Kx;ZPe-rA_~{|^3codXxkegX-6 znUv$1Ti1)<-h6ziZHTZ6D-Erg(9^-0urI2#3^mZ$`Ur;UlZ4KJn&ApUljy1(<>={V zKd%Q4WMz$dI^JDB!X)?)rN$?QLl)iF&@Kuc#G0}I_R8(yP$qx|WnDw_{)~;F+bwMF%@9=A6rn{fs?yVdWWvo@l!l=zdS^3T zw}UgE1ySVrGROjdJAv?MP*6OG`~!?; z`np92&YS6IjGG~G-ZZF$IChFTZwomt^DRuO7ZaF4p5?3uj+Vi-amCVixiIn7@u3MK zIVy2A0R%IgjU zj7)&q5np1QbWIlDiTb_?*4fUKICASPu*AZ432^Nadi@9(nBukX$h z7YFl?TC5Sq9?L`+kx)dkUe)-gt1~R zTn#BU@2#VN6!9Q+cMrW2N$R_#pb9-2d_0FYm3lcT3Nzg9SA4$n^<|GC><)!5Sg)IKmBdiV->|%0)XKudy{6p8@ zY=Fb}tTUdv&scR?pS>`{>n}T)@%gka9x#Gi7R6x_`x%L30xO^kYatI^p9oj_D)U@V zrT$i%{ozD#bHk-C=T?$T6)@S~e$@m~t6trLfjWF(25L86QP57UeuCA+(Tn6>*{0q2 zIM}AYW3=XVPoXxSm`8!@l7Z*#V!vi^o$Cpa#Xct<4SYgHnvgWfJh)^$=7NU_OP@Wi zqv7Gr&n?)f>F0E`^HioI%_pPa8bvsW74DX$$J=fa4;;qC#Gs=9Wz`EZ*=gq64#;qp|Ws` zNUA4~-TSNJB6Uv+8f(No_55-uq`v2up7wZreLTMT3VkGa+)a1!3$Xm-VURGHcqB>^ zjQ%210zVWHejHJe_(FMGd7B&+9*+(%hVC2Vj@;VD{{*YpHF7KcHpPVv5&17_QeSNh z$XBac3rwrkwA#GO%DGnTi{bQkh9$D=WVRdw6bP(p^*SGFyK_TkGZ*$&(@l1g)N0+P zDpuFJ!W0~Vy>8_;y6dRaOqw@6z&8({9KfHAuob#jJ38=5J334l*~v@ILN@n0lV9iF zDznVIJ;BU(mKAPgpQM}yNgPlT#A!fiCK=(9`a~$jBy+h+vhB>w4=O-S4?9*Jg>hQBVQxZEF#4x@;}KFwTJXiF_L;W4P+DL%H-crUY;}*q_qyN(_ubGrM3+#*lFAHE2cg) zF`0)w=Cu#M8|M9GX{^jlUM&E&uh<#aAmDDOdmIsGFk1Y((FB;%=q^*^SB!Hz&OnW`X5xe}Ro1Q_=SgQ5H0 zdcm<-69(*|f}=x0daV0MSa;pqTi!18$lc!XpC3k4 zonr=z+bT$*Z*`~Mq9Y|3d*Mze_g_WWYq4vaN9A0ohR;*Gak?uBuO`M)T|I{#2pia>aUhXtLcm zJluR%9t%7Sb|D=!@Q_ASuq0%JhklG9tYX4bR4a$Bh(suns-}mB>EXe{MqM*gf9mt{ z4>fGCyC_0On1J4%t^SX#&>~uIBt&L2Qn)j34+wdJJRm5y(R1+;lQnxhI6Q}9U^N&H)o!d4bREdjT(q@bjqQrPYrq^$gGdS zQBQr>i%;F~RNQb?5mk@T8U7Lk;qfH!H{fQwRiVhJ5?+vid>w|ciiSQXNy-^f)C;)p zD@l2>6)-$3g`$5KHhkMeRNE6W^dH#9GQLNXxVVyTUYj{%%DC4TJ{ln0cs3pjAf&sW zjR!##1sRV>ra0;gsZW>-kUGv7=Sl=o=BZ~72$KWk!|B8-|8U+!j(n|yV_Ln;F0in3 zoI4kAQ|Z8iS*2jnC?*(%mt6FEqY|r0+*Xc6Q~xBd{$*wR>sGwiKJN@PyQLm95F0yk0;H;r=(XjVPFlA ze4+kXD2bpB$-}8qY5C=BF&awLfc^sIv>*C2docm3C8XuV*P_vurNM0I{KOn`^X9DKycL zxy}_zbF-^&_A7tJ^mVK0!;^_EL3!dY#|a9@k!UKYshk@Y=%N}jpi=x#kHACTAavu+y%=jbgqRbj-u2D(IAUJS>_~^x{Y_ zj`ZTle#DW$&uA2fibR5@$PBJX;$|sHW0Cq{Dh2bmBf;;*k>*b?j`ZTlmg30fv-4=g zk=+k@!ayc8%K{?9%tKmdPFNWEL{b?kkNMDqycb6f(CVj&CV7kX&u`P>&HML(zl%uH z#8_+s9BN6Vnig_6w7b4F3@RHh%oSdS>l+Bc_xdeUfSb;d4Ai7Qkg*RRPfnSWB<8w( z7Or4-^PiPYfUfodbWan+cwX!eyhvYrzP^#-jl|rE9uW71K4VDaNqFc9LaE0{lEU_a z4);7@iX}6#0@=x zI6TmhnYWsNOP+`zUBgrv$BM7j)*O_}80c5$uNHUU2ZRPl(Btv2tkqresM zAW7rh8CNsuW2gfXg)x9K;d5v_%1KN^mnAIqlz8?L5MuWL8CM&gs&fqm94qMoy3mHW z=Xj{Ji|&YZ(rL~lXZE`M#!)$}#!Cl+o#UN>^DD2BtOd%{6xQa3gjel}w`xm1m#2C4 zq~Q#(x+mD~5kbsUOZ_qU_eBVMv-iDyAgwUdoj*8zI0aG`(lE>Xhy)^~$j#^yE`tCr z<17tXz}!r11qFHqQuC)jIP4Wl+lIc{cy=BI@Jj|>vb!B)0_moa7n9g?V>ALyNg4z( z5i*JxXM!skJ$vBi9iSa!b8z@RFQ(s|KNWXgSqyYP-eW}x(COwAY&P{(ImD8N^8jLr zY-$86BvRuKVc{%r(fq6Av!fwaw5N>MP?Fz&d+%rmvU*~?w3sa>8gR8_sH~VBB|>qK z=Ly550#d8YF`{aAoq_Y`r82q)fobEL$fa5q!<&}y%H#m};1a8>>HEuT09p(_Dy30S zXr+Yn0ZP#*pm6cjqUJYeEX}cNg zI^TuUf=wq^WtIO6J^5?~wNVx}#n~96J2>``2~3AJwRzhp5+9b5?1%|GxI@Wy0B;b7t?BZ(e%uP67@k!8B0m6%Oqh}=Yc3sFQ>Uw zr>Z!`Z0(hpRw#R%jw=`u(1m0)B(Sj68&F8xb?TgPDMBK-7ZRF;if|ExBnvUhdg8l+ zKBUm_m}OxhNsr6)CKJG$d(~2ogqa#C)z2 z6>i5Y`kYP=A7ayAb#l0IUmeLN@B6#QPNy-=Qo#Trg}xw^XPj^q3L>c=P$qn6Kl$u8 z`SgG{`8U6ue^=F!Q#DG5X{SVbMS;DRO=cQ~#w@+HHP;TY-LE7RE^m$1?QSfmIzD7x z0Oa1zJ_r z4!+#lN3of=u7ctXwL9QgVdS3931&lNbb>8nb!Ya_vqrm^Ijs#e7E)-lM(<;_Y^a4Z zdNJD)HjYrEU4u4ZLl}GVrU73kPHwV+JY zwi=ZWhFCHloF;@*qw+xzsK*6Vq1J&VU9$^qiNE<-yBu9-B4z-m@LM`hYqeR-_VqCjzpn`}H{-UXfNE$PbM4<<;A@Nd5 zw_`x<)mPgf*+pH|$94|f3vV~?$s>Wcfw#NCed0;s#(qRZ;$a;3Sdk=1GLpKfABHqc zXz=VKOD3*&xbU{oVIgE|jv;?8>ox4_H-@?$Etkf$GqsV%5wg@?a=KVUjli7JoS%Ko zB^Fgjf?Rn#+Ma|Wz?nN}t8E;#lLnP(*xJFi&PZWomAoF+-B^(32(_qT8>t`}U0w}j z_3@=XSk(m-LPt#2C1yHII+|-)E}Bx?{w-_tN`n)zK2HPrJ!LecA&~8Z3x0Eft$txn z`}*U&{ox@RWr<6(kZ?aq2@PaKSdw@o4U$-7VM?<&?D5c};i2u0^EU6qV}U|`v^&Ry zD&922h^qmKnJ)R^OcEXPw5ckcl}(YcP; zVHHISu&9FgHF~JB8LC#H$)}T!DVkQA4YhV}#EP3vMN9>H zB=pco=-Q0uj*(LXi=Y8DE~yf-4{@mp$*`fY5E^YIyzJON@$f~dMvC*G2^s1PuDkPRdALP}hY|Bn< zy+7llnG$uq5SOQ0u7B0T3*lMp(T$vwLS*O#eT_e&Py$9Kc^ zPt~->CGSnMsye?<@}V~T+#63ND(JY$gz}otaXeyIRU80d<x;`Of|USbqHHR3}^X5hT)xc{P+B#Jk8lngQgMEvb$-iT6D!r+XZ zUS6U5v0dz&DwtCDbb|Kbcam&gbh;6Qi%uu65H9vCTWn(@7-w!8(10XJbwD`}Ny0g- z6Rt?5mt>*qxxJ$PkRYIUHw3(Ox!k;3^Qg~z0$~3bCA}xq-#yCT3gvIU1|JK`Pj)h9 z8IQ)5)mE^6E`fP?e+hf`Jkab82*E0uLtmJkZ*DwIDpu5HvtL@gnkl@r zE~4e%iY(jb9#-GIch#w;qfNfQbt=!L`~y1kn$j&-sI2l!Y>1~&sntDMPL~U4y8psN z;hh@4`sKPaE&I5)wwtP|vReMk6#Hbev;g>C==3fwK^Pd<7h~uX>s57I7I!Ar8K!$l z5~k3K-FmyqmIW{M1iRc*f{pvx#?>-w#)ysgRgf`TF_5-ON(V9Cyhe8}PwN(Be1x3&0wbqn+VuYj>iPZVOV2ds;_{~I@ z)OV9D*UkD02Jwm>YHH@)+oZVVUz+7-snJF&+6-4*G@HIy)e@_-XnIS}{#A=SwD+V? zlf{AY!)3X2>o4||E#IsOXSG_oUzAe)RWIEh-%PIb(44 zCuR9%)zH(bCy%b#|F`UYcc)JMHB-MGg4~xy;H{a3-WOmM-n0&Wux18!J-EPt^c!cg zZbRKMiSF&{Xw5vVJ_xpFt?bz}pJ?3%yL}_Odr+-g4=Y4%oc=nX>%T2zxVDV30_NW* zYXoOrIEd*^#0oQOkqb?^-GxkSct$-Hir95YoJ1b+JjGz0lL3jtEDcm9Lb$!1P;zI! z3u4N6JUd_LD#}NBF@^o>*Wz4MO6m1Z-{qS+tXRm4w6TvfkjI^LdMB)SVJ@>)*V zXuMLY3*3R5Hh!C1_3zykURS!DE64U{NM4xvN@X#LLSKStE>gk;iwO^6U#OU=*nQ@% z63Kvi2gnq6TWoUFmQ}m>OrXJCVWK%T2QP-AzS2o|!c?0Z5$%R6XDi~Acfyn}MT=sE zWAt%U=j;YS*U3qf#kYU%Obl`_{VE_HTk=Qft`aHqC+*9akn0c_Z3rT!0 zlzxzTF*I_2LUuqWWZpeSQoq@ezQ3~)?O7brn&%+b7^&=9%x?qY)t4u1qP zgXw;hH z*yQw$i+aj2xr+>{cl&p{3kGR=i(ufgH0HjW5$aJ&Xo@;N75IeX+A9R5p;TK51~79c z$gjPIbZ`WK!gF^;6f2g+)sSNI9y|McI7H9q-&j$Xqen#*vn+dH(RtJZi|+<3s&+)Kg(ntYe?Sw+C79R*ljN8O=^uty~M1Q ze!@-W!el<^zzk!Bq+6f{1 zyQ8H0kX4uU*$X>F^b<3H9?;Y%Oguo5UPM5Ij9^PZ6>F9#BGNeJ84psnK-2mJO^uGw z48^_24CSXBBoVXlt*q1fwVJaFI!dHYUKPKGG z0+M;5@@Pimgg$#7rYJtZaFlNgQA%rK{zlo~xR@Ec^4HLoNH4dmHN8b_0+K@tqXw?2 zLh~I#-cspbT0U=c)q%kmE%8Om$7BLh=CklZ_?@v0P|mq^oSILq%qAEVowr=M*6_Yw zSsHX>jHoV@R{sDPWJAD+stqk}sd!Cj@rAhTJz6vmpB!488>P!|k=W4z-A!gg=5%)V z)ZE=2MAG;c5Shu?6=}kVpq>kRl@x^eDj;#>iP%eM!bG+eh#V=IPe(nD?EKj;j->A0 zIP#4uc{Z(a2|ImbMwD5NVO+hNshszRFsa)k%*PALu;Hvd7KG_1yPqjiDZ@~?Bnu)* zsOxjWJ&_R+2C1L{O@c7&5oUCF2=jd@*sy_Pj*T%>?*b?-n5;zuBTYR|oDsm+>_$f# z*YN<_DC8I>vr;2K947pwb;mR@6qX$_Es^JqGlXO-DE+AXs&oVe3|nxW4>3T4Qa-hssUmxwA+*;M!V&pdo`_|Fck6^cSlB7(!qj5r;mfaSw!rP?v~fT}~4gs~^X zj6ipJghs+AX%bB~4C@d<#s%_rrt zfVyCJwV_g+C(4UR2-uF$3=na`5=nd>_%Yx;&y(9xsvH%liw_UfeW*>)rw@kmVws6! zCxB;D?~-Z&D&XZ5nFW?83F;2Hh6Qq}#c(;GH`cIJur}$LE^vtZAV3?V4(w34qe4*c zH_DK*8yh1gJ3-xhgl8UpH-xt}KgFGas#cO$z)VE;3J-za=O50_yw5*8pn}g;F>D|% zC)5vJLK*x8|D_~K{geuis)X_*Q1BfS+Uo=NJTv6C`5ZkK)P2LErgJ{mxDj!@A~}9M@!6YgKxz zXtjDKvd*F|Z$oA@TW@_LEvIZqu*|xAI(`ra{NXl)5FQnCRlpF^VJh1|s|^mtz@~qz zoqRMdD`@wL4q?h_y_h;TFr*z-e9fz}&?-l5)d)o&;ZtlOHbSmntP*M<%{ltFF-9gr z=-AjWmOY)?1JkK>#^s)<(;m?35Cv>cL?j3T*s!7~B#g%j=uR3i0 z>{oRdtUCMd)TcR5xvO4VDi4F@Gw{P>I!o^A?Dux5|6rH;J@t9g_xUYPd$C~yJJXGq z;-e88cK0k6nHvW_2w6#{1fenY3G;#gS>xQmOO%SkXU^4(V#5Ji0h}98n)53)92(Qo z6{lY7$qkJmXnVL10|+?~hBru(j7r|~|9$@H?FVC<*}>%uvHQ3J3_ets%7Ikb(256R zgyPdegY$--4a3MQ5HyT6r)ES(Ls3*S>wKIVD`Qf%WkD={$?~Ep!5CqCtqJyfU9oxi zl&k?@IARjUD4?+?UNj9b)XHKc8&>6Uc@5o#V?*;QKcJG>^?gZWNrF_wgl17nm>>A? z3u8yeEMQ_gR*}O}5(jrdy^px{!^A8WHqFK6ULWNJ4~+7{dn`bIQP<7dxIcUs_Ew&c zrWW3KRvrt!3wO8jq^Y2sr7q!8_{bb1ND>3wi{mKv{LBrd+ZzPX12hPDB};%=Kj(kJ zD&(wQl-EOdhIXZPu94ZQxU^^RSOX{1hVWY5+Sp*S>7+!9cmw~2Y+B$_fJgbJK!DrK z%^rT4hmQxpu(tp~O|H&feE#9}+2_vywJu=*Sbv5W-;(I%=g+#E;2%+y9QyS)#iXj7 zH?Kc`Hb2dy*KdD<8?u6b5mNz7)2HxDscxO#%IFu`r`OJ|1~1~hd9aAq0&`e!1dU`B z!w;v>^?4R2jQJ!LVMJ&m72#fxlFW-zo~5bClIW=`bdL=9QF=G<)5Fcm$scEeqw}X? zUkVsq`$L12_Gs{t(BS6t^jOfKyE_Ar?`GT$7$0>+Y4N&)^mJp+9oq`xTZr^8NVU(6}lqnaJHKn3t(z1 zEMx&Hn_zBIHAVtU1(~xmV8snISb^@Q!gh>O0+ds#`dbsOb_2e!CU3~ zwX{Lvf3FRF$bS0XTlW0gGZEH3Mhe!aO$#%x%kjXlxJPK3x`cTwAUt$w%%jW`iT~`SmLNGisiiS2cwOC$C*`Y;@0{~- zS!Rfw|CkP;5g12*jT`61>p$KSQ?1DL9OAs5R$tYRY&d+mv0(Gcshdo`&Sj#$fh15j zY~|M=K`fW=h5i}%0|{9>FeY;x;I;F1IK&);DVY7>vSQ;aDE!;kuU~2=E=!e#ahy9L zsExFwzKw@vKGAv4my4q)V+1c^rGVcmfqn5%gyKIu>i|xcVHO=KKC;fkSKFQLOq|8G)yQ7S?Xqr zQ&-4mPl1(02WT9ztp7F^u+U(Do_@>=HaDr)1CIJLRBzWN8BMNlv63TmfJ$sQ2lKQo zY@_IgBD8k2`pB_HJ+Rdz0>o<)(49I#f+LGAm$d60-8THqn^FPv``geEY8l5qKsfSm zMyEMbwp&ctvFxx8d^clO*N6&-jSJuPRX%~Wv({E2n9Rze^@z}UTtql;1`A>XbWLLk ztG-PtCYq~UHD(dUE%J*JjB69wXQSzk0u_Zyz|I$-Y9Qn;NitvJuYmfgC&hx1Ko3g~ z0G9gRDsQt+t>zYTJoNl_^8ij-P>jP@dmq(7$5Zb3LljV+faZJi75Yd76zT@M)46d$ zW9c%&TqX#O67z&@5E=&w;a(_7B4ZkbSsG?p&*ytcaL~WU zb?-mV-#h1efoc3o_o=6b?2iKmJ^?-T=2P=%U|_sEw_C)N8^$S#nGcPZL;+zkObO@0 zOZ-^Io^YQx41{*}4iEqDFz=dFufIU*K^nVlO3` z&ncl^77>H0BqCTiP9JjSo&O8(@>tZMxc%H!N#y~7R9xgu)IK0+-|y=(kJ@X764U(T+L`5 zG{|Gl7j=WqYuxH|;FSzEUU)FY#rKNU4uC0C2 zvbcrLi%ob{g80!`E6BVa57|u(y)CY0CJdJa45j80W`uV4Y_7}tOY^fKo;ub%GJ**29@nb*CHN=1RPGO5 zdQXA)_gLkNr`Y+2C|W!L8-vZ4=3@~p!rcRh@Hpi(27$tjm`f;SK4E^$Ng#b1(U?YQ zte(4Q5xef;i5Byk5iHS;JRq>3sS%##w-JDyLR|w87lyNRr(2jTuGhfpEs{EERO(d;hH8#Pp|z387G-o4%h(OIIg>rrl$Wyoe$o6H1eCY#LQ z|EiGV5~ft$27Vij%3^rqltN4^!nps+V zYXST4$zePkdbdC}J!;GYGnjBJ?T*;u;1A)`QJ4oBqrM z!__fx5vfhuluZC%rFAQlQowV&OgPke@$Tav^d(>&mSwqcN_e7A%du?j>!{JoD={5u z3)C#H)HNFp>yy@ds8#^-@F3)}DyL(3ptU*np;h7yHGkIi6)xN8>VqZQ%Gn2=|1!sf zJFEV*Fu2oU&~7#WZ(FvBnvoS$s#dHLR|B*VL;Yh>7NmVs6_=18DkC9HXpx&X;ltqC zWytAtB!F{XHJF>OM0x`KORgXzu!hERO~~lXwth517<{R2%0aD8O$Mj8H{e?6(xslH zl6pw}k`fvypK#^{B#aW5WpSvu*v^J@O?27g4d;jVc`^NVFWT6=r;Y?|_dC)KK649g-G&jH7~4QBiLoq%Z5Gdd48i^I#=NXf z5G{x+Z6?^dB2No8om`bw{x4*@yn;L?GM>@>z8~Wh!#2MNU)FT^$`UJNa*wWF0 zFvnn|&{@*JGNkw~pfbe`ymQHl{9pQLRHL2iw%o=v5eseHo}0KtUEncR0&O-iAgV(5 z=;jF17B=ON4CSwYch$^T*=i->^%XSX7-g{ zw{IIwdYtGwqa7+hNln5D!>}2XL)$!f5PntGy7^)Dp-OZ^RQ!8`e(mV6jc;S_%BeC* zxA3+#k}V5!!VX(9+|BLsf(=ww7_SSSLgzqZjc{II!%Sdc!Qur=gKh&yN6moMGs`ch zm8JgL6)AgLLFJ_+J2-ghnO(Bp+ZF2{B{!SO5J&WdSp#8YTwsG<;6S)gqCQ9*y_D2C zc=-DIdSExEfhbQer@2(8syM}L?G;Ewcn*I$t`wg_zmd_9z`9-!t|p`5df54Phx>rW zJoQo%MPWuLV}kHhB_v7$jK<^#)D>G{=k?Cg9}_y;fp*dCjr;e+@g4$3e|Uc{tlqp6 zj|NunZao)zp({c1AR^>3q0nR`VLl_F0&pD$A`Zo~ht)JbK*yZ7mXDn{ZyJD096PI= zxArV_V?Ewt0v+^RI5x|H^BVBV7p6V-WzqXWPl<0kZ!a?anq{a`DvLt2eti#6E3MTw9KVunXz0*%~y z3%IbbGXi+Mgf2b;y_)I%FHp}jJ+(V}G&3g7Y+cO*^Lu~=goTd{a1}di)w9OLZb1vD zhvS95so6zs68gqPJ>{6hMFv%xfA;~8dVSXogcJ!$WD?+B6k>EopLkiy62)l}dfV}N zJ|AVA=k5w4R;+%jk;CS_bri@U9z=Uk$VF)qr=Dg)lmNRZ^0^2vB0_m0QAxlRf99kv zh#U^kXzNw${raQ2VqbGuIUf;3Ea%?iiO!=QPwWX#%wq){6ZM5rJFNv6mOt1XHPHAL z)DXuk5CN>BiNdhPl2O9r5VoZ_$->Z+N=4~bs9~gJJ{|SAVb8c>^Zq&(+~Do5aY#fK zOR7Q=@WdrlCCH-^aS~D&ZX^+<>9hCOqj7Y2xZ!nsq~LsDMLwPycj`w254Yss!-vkJ z9zJ|Gb;P9nqKb=AXJoDgG;aIs0hJWOHvk$Nu`3J}?bi1zN zvEW3nt+225*y13u#lfj7Zbcj-RY9U6N`j0ELV1SKLuf)=6?rUj74tIIBaXvC9Gmyo zu^^6g_mJ)k!%aqv6EB5!qkfbUP9v9arF>7sZr}>pBM$%Y5XYa2JFhy?$l7Of%WL>LvnU1fHluKTRj%m>iltfikiKLDn^1AW-FdjO5dU<)%t(^HDY9*%v{az|S zJH>Rw(ZkvxqE2$}3`8=;H%{FkUF$=r6a7Scw{*RLPMo&Vrmd#NedolcJQvEc4Q=!wzpqUZ_nA($ReW7XL=>mPtTD~i)7+mLH@-d(Y zrfFR8>J(?j1*W$f=!rw6;!q|;7_Gl22xmz`lnPzXPg%s&c4sut3G{5@kn-x|hXuT| z3Iwe(2HCi$jt1_~o#9TZ;wbWhm022k}&a!@M$7hB53CK=QI8R8j-FAcRv1l z{>k~xZi#O!yx0Qp=ycXaJwc0lPWBFa%ou7OVE`I72Y!02(_@{($2x%?YW5A?hXVuU z&&Fjv*)tFdb_7B!i5XSYC!U0Tfl@9BcX>u+%Hu5cGM=QW2SSGcgf{P~BLPCGySv^; zQI_yn1;qE20u-5gB#kqGk`WJhJMoCsxVRYByO!~|#!c_C!;^w% z1%05p@3d*}PJL`+R&>H3|C>4cnIpNYMWK;XM}M_jmU<%R(aEvM)Cca^vK1 zxl_?PjemF1KGYw^Z;Q)Wyuw-LD~dKQ%VGWX{xjtG}#Qw#8)b98pcO*LzME z<)XEQ>I->2TG_)7cw`zN1Fw_`aZ_~AWl2DiM0h0hLXRab%>r+`7%^*u=05b_4x#_j zRq=1;s%AEZpG#CbERy+bTC=&Rf9Y~8oSuB#WV`o(;u}xWqmY*{WXw)pgfmUo#k2-c zchOPYu;H2K#;%`9m&8fr5zkYMa3%v1hZ)*DiV$vZB}v_#?}Cgq9^2Rt>gxp(V$X?A zqr3nyqyDux2N6c$aCJerp)~0H>53X!dG~9kEyu7g{B9>>5W5K*jaRgGa^}VZ&uh3I zw&~k@D!pE>FO=gUdL4S4vMfE2USAY#S9N$yW7mt?Kk;21PS@`6Fdf86`29P4t{mHR z_%tUVc~!YMGo3^<10octn4Enm|CJAi>@*y>&WjIBz^Yi*SN~Jf%%i!baDF{^{u3m* z3y)qzFZDr%`cv`$$S0>^5D$XLdGQ}VfBNwL#2Myal=JUOd?{Z#uVK9$snf(AxPu@~ zV8}*+bIvkW<@U2}M$noK-`2O&#O(}`F#BS(}h+s0BSbs zm~EQq5!+zrg=p3WILSf0;1{h|Xno+7-+2Jsw+hoZ3L+*BWEEa}@AbU>?6>JzkPZSj z`QFR#LOC9!r+E^lGB}W)UT44cyyUJvcGvD>8V!8vfB!z7E5}3haU2Gr2$Cn%$8ngr z$(kT~%?=OUvoIQX3H{C;zF3Zj=*jy<8I@O~(EH^Sv|MR~?{>y}*A zv3F`hH<6hj(Eou-=CVo`%-N*`RnOQOxIvVn{L(O>+}qrrWAc+vYE>ECI{Wbcll8M|m0niXjE|L-K?PjO z952CJc?lP$9Dj`lLjm2U@jtB^c=A#$kM`1d2jc9HyD!f%!QyjIjx?*8yH_LV8 zQ> z6w5o$pnrG2_hz{#{hGXgdDMAxaVDx7$1n1hZ$U5hX6B_{>_(BzRF%65=p5)`n@{8>Rx2JoX+~!jSpP*V@e0TRt-WDqt zMO&FQf1_e^?EK^QnuPW?dV_KM%l|#tni?@txFl|FPOzn@8cr`P*0HCrR=(Ww@y!|P zwz1__tk@Lc>iId<0xS9)=j%L|C+pAFppgEnKf!PQ)yhpA=BR2{jqa=YOx`|Q{&J(! ztCwb9K|d4v5biIVvd{UQCtc#i|4Y~Q-+#e>{`)U~`LA^mg>=Fs3*3aoP8KjfNC8_! zLNc2NvG2rgVh8SvM-)l8dx+xqa^B40&^)axSXhnqS+!SHePO+;%2FS)Yomz|5_j(` zSB+Jg(s0f$Ve_)^?6l^k`Z|Nc0bVrMO{=CBpUn!hD>ozS%Or2CDZhr3T;f$YEF^9w zyw0JeeObIgNYJRIw=IN6Ro1G~DsabyBU4)G^`Zo5Ra;r=s}e9&06e5QiY__)B|BvC z^uS@dIc&Jf%c2^mu!{BuIha%a&sW_#6#Y6hlLOQF8n^6|ZmF7;`sUA9G?6pCkVm+{ zPhNk@MO`(p9!7tIN3){38ojHgGXMki2-X0ChIEX8vHK-t+W$Fi+FAdj$-6&g`L+8q zTU^Z*uBpYZuK;798RmB-QvqGcy5IGnysuaJKY1H;f(yMv7aIU=+P3Z9dULSTy{Aupc|*HB(gd&CcGjF6AM+OKy%o0KdG?S{pCI`zv=5B~OHw}O%+PowXY0a?u9%r@_-dci}3pgsO=@hUA z!+&5(+PW%^tQl@Lup^^BYYLkvFY{)4M1X;}91tfW!&+C+l|8b41H4*XTk{5X^1GV= zczCQ}MeqWjH7(9dGiq6(RNEX8{8?(}yyWwCQq}oCp>!??;FB^AXC6yO#aY*sH4rb{ ze+$_Dsz8Vib6vnPLEtrp$sx2;Dm`j--#fzUy*wbQc_9Jk=Hs!#_CWviCo+1qDX?}P zg{*HCIY7;~?oGLvz(Ms<@w&{*@xN86{?rScjdQ&it9f&7dV7v@dESllxf0DzBJf*9 z-HAS}1$?-=x*D0SVqWx}`}Dkiy|#c2>F0k@wJ)P7HRV;0?cVOHH;NXL1+u5DweL)+2v zrB5FpR=lk}iTeV4;$(Bho1ZwIV@E7;;a@(F6-#;Iuz2hyypUZ&fsgz zVJ5x{vt$3 zy|&7#Wc`z>9K*=LMvG;_hI`!?yg63chS8A*ZBJ~D%eF#2ld$0E54Zozdo%E2G6qUx zKbEi7#xklLUF8?~Ov#*+!3BRE(^vFrGk2rJcEThGL*}}n&HUK)SsE*!#i0`jFZGoS z+=IK((;9nLV3QSCWc``i6D;D|{>E6uwj=2|na?5_M9g=CgmLL}mN_bp;vfK!_u^fR zqFr=e{=6M5^7(Eo^0_;U=>#?|!=I;o1|XvR}90^Q#B&$2a2+sY-NNqE|>J z@7`Dwa-3CD1u$WVJ4lIPz!o`6Rdqf_>yf$vJr;lhpX{I$^b4DWm`qOdGH=i{MH-uQ z-s-{;)1aQHK!gIr0tYQ(I(Tzl0-UM6Q<0bXnwCQe|dED5ui`C$YSW*kb!!-#uc6bltS&OK=g zO`T5<@MVkeW&OF@7x;oA{-*E+ke{E$oW+R0FLQ4FZBp@RzBYpS0?6ZvUL67l>k2?gw=>_yHebw?a`VCCal|oMkFZBAJD0^4vX@qx(FS`%Pv`p32eF zdMewVcfY6dnGW)xlj3-J8{L|xGBo1gsr+5g-};~DzMwzf+Zg?YFnVqTfI0Lthxsss zjAtI8zXW7y&TXC?+;t*<2bEVpe?IF~Rs^a5!+oGtt=dNbKtMkDJ(-xM2H>E=XlJYY zP!^smMUKJEjVfhhVI$g_XIU;Rnb+`UajP*4q{v}m0=6uyR89D0j^RqA%>ukdQE{e< zh2L4GS7d7z1eiC%y46%}_T7z~mBDa7al|1BwOwIIYUO1;A+38$~r9ll3OU zT6CAHnJP`u-DpBte5lCdDY*)1017}nujU$(&dWX=aXpOu{wwPK6!ldr*cy#y+kVbT z_o%0+kAv){XwA+m;<#pAZl zrG#~sL&r%KXLYlr-KBDhpx2xZFj5H9xmBwvAk9I=iv`wOJfQ{Mq8dz*8T1Bf7aR-A zTjPk8!WWHH5U`@)Aq&4Tw2D@Rb}XZhp#j0DwaB*&E@AapBStv=Q+4u0x6N38oPO@z z&uBK&y1K#+&Wib%<)t14{D_vmA?T#(UibA3eejzRT<$#JVE3?^xtPEMx|XV5bkKY} zO3%H#IpNfwyQh37eaZ(cph(Qe)_0mxu9t}c&DSop^T6i}3K0_OVm+Z&j@ zf4RIYX7d!p$GkCb!ykC%_zaeO&WqD|+FMDEKYuv;GCDgQ#Wp}khqjsQkDdE$lM^dr6W}61pd?NkiUoQ=s5Suj_(F%^E-VuZ%2xBoF|qd{RgDT zcXZakdH6f(zA!>;d%~l?C&Hum)Idb-iVKzhq?M`oH~vVk|TFR<~yEZ z+|N9gg@LDh+jpY$#S^zh7*4!v;^S|p@2uCqX{<>e5E4MQ^$zg;nq4 zF`5a;nB*2o>k?r%2)q-L6nHVM8gvUqPX&P3z0?aZy28fXXz~$CS0Fd!D0L~=D@ziQ zc|BH$xk;P-byie)YwRb3P}m0FB^exqJ0hCt0%kFug<(L8b*b>lXTc7$M~L36=Ml<_ z4MF-=%xE%iVSUF7ehAoq7}gt<{*-)kZsK~r6S64uIP(L~VQGpX4xX379ajmNKBWWb zk4L{B%%}U7KHcX5e4WI;ANao48+bl?Zm-%VN84t3E3Exi>>7*)%lNXPJgLnsV@Xx0`4*pp+7i~2|Jls~I z0wiqorauk~L+d&$e5)-Zx_v;y8=>Lem-%J{*Q`SG3u^C+qPn7??t6D@D#*AoZ;<<$ z=96p@8K34w&S@ZxYx6=!Z&mn(^_uzt10{jQc{9_-A3e#vDZ$uIy5YUc=7t84X3(uh zn+jk6MN!SsC~}$CRXN2KNJa&^jYb8GkjJHjwT@vmUh!*+pKmmoC3N8L^XaTN`7GL_ z8?PI-JX$x~Hn8rguWCALZy6|br%lk{Ue-B zHK2e`)uqB?Y)hVr@e@zF>)b!-28kNho$c|aLdDNu#=pBN#XhJ~4yu&xt5ViKnD<4M zvIXW`+fLF<1|jnT2L{jcBbFv^%#tKdR2undkn)2`aR*f?XPlYA5tg{J6 zhsZZi>8QGnI;CqI%1Wfyz|HnGIerSw;f8$x?Edb@2@QeU%3NAV1FE|&4;y+tj2Fiq~P@ci2G z5}Y^R58k|38>NWd)JMM9c}Kg8Hb+l z+YyT$KViO9iY1-Q(|8aSAwf0$J5y8A9m-<{Gia{}8=1q)pnF<+$#i$lO@ zksk*tjwE+8|Ctk|9IVlYV7KQS?6&q7x-YWR7E&A8ZjwZt$4sS;fWeDnmbd|DQRsMH zCPOcd;)Cqu@1E@R1x~*mF+pKd?zAi6O-fwQXB{wlDqW`LT_ro{fKLh`vLoeuT4%Tvv7d208b9ek-IKZZv6XnVH0z z@pb|ze^?!H8socAV@?U5kZA=^$jd61k4tZE=?OJNG{cmX-^~9pZ@YdjUYM8NE67Yp zYU9NlR}+PnJax6oL3lPRu1D5ShEznSfY%AEPz2CyD3+Q{)RbJ7078z{(SVKvn?gN? zHlUP-YzG`(gF(c65GeO_!iuKlu|B{3f)+=Wb|ONpW`)Tl&%2Sin&j{jGE38BqTH() zsQc!nDD>u~z?lK8s@X+QXkW04bmG}fMv**2e2TKwLDu+z7Q6(YA+FcN_Uyg4;oa#7 z8%bAqF#sAR6`-0_W-N7loB4i_*r5tV;M)gB*n{ZtJRM>8bl#o#0TbRQ*dlW_(E(Xs zx5vy06(8iy?<8-ozccTPyqRplR%Z(#eI;2I_$Y7MAgrfB!cv|FHV-4;OCK7h0S6fdCZR#_hLJ8<4hi~-Y#b0Daps9k1fG;5_IoDkjKjk0M_*vIBc3p zRbV@VWq5G|2EMgSHgv1YhHm}6N*5=4uL{?+>T}bQ-nj%1cb)O3xYiE?w9{H*K~77? zTrn^Cyd|P*C=?U-jM15=FFn0peu8r79a)8m$eo&qTtbkdngVc{=BT-W=^`8pBTlMj zILoe=IM$(%QeE}!*6MR?v`#T(rh6%!F?#2aF$9(?SwK4XGTqo58{Si2;QKgeSP2(^ ziA$4E5ih9etf;OP@|%1i6*=*d_mBN!o{J0gcTxiVi1g^`?8!O};mCTg%|Fzz8k*iY zy)F8KqujHZFT8d7p+(THHU2n7M_s}pC_}qzWre~VkS)@NlY5XF%=z8blJbl!2bhxy z0bhaq^kkW+P>4|0U^;a=#T5$xEmup{pb5tz5XQx7FuffEKGvX#WjZSR*}O(4Js9Nf zyICF`O~T(T#94k>;fx&_`^z1x$#_~B*S}gDm~a_(YqFzkK@$ijuOc%^Ng-Hk8lHad zEKFu~n87#J5jj`7=wU&Y9Gy}_HR|HPo68irJn1J z5|+<7j`}&SmUAVVos`pV74@ww=jQwxiRySf&!sw6j!+W*Ii=AMr6iVR}yNB2hxrZ$J^|RjLw&xyVdk5|DchVlO z|AFocdJQ)B6bL;YNP#iXSxTz+h^1}@5Y~~tbka~bf%D?g>n`SSKLrS&_pbMsyy+YT zej)T@eIl?|KPsL3auJ=m5Iw2JP=fV3E80)*@SJ{QrGO6V>&_vXoCFBe@qq&YazS@q z0velHvd&RGrH^@vLxm5dMBU!OH{d&?J`J-(CDn;@8g;w?d|;Ax?i>$>+tE3(Jqdit3{%q!R>PE|e4 zn}&iCK2=hC3vH852O4$AoBo|J*ZQCBzF@9!^UUdi9SFdt8FSMR0ILU8q+W>H*DQ41 zEXhQizIeV1PE1PV2f=tx^`(IQJMny@AgYRD&ZR)wzxktxB@o4UazjQUzMg6?1hEYa%# zHZZ?u_&sQ{ACyV7MzrMxELu3ciXtCVxFf8}W8AckZTFbg6s&%hUe9>bFeC_K z(}pf@!^HXx+Kuo4karCr2e$oSXc}(4*4qpv;-3LbUf%pBH^IVC|3Uw_JNn0PKe2GU z1uP6_O%&QTQz_=qc6`oYJbfm42Dms)V>=EGSUB22{i8mF7wi}mr=)i^Y5n@;(?@d3 zUmF207eNf@*6Wd&Oy7{RUJnQ*`pD(!oRBXf(YiGx1p}Z=W&DZ`bQ<(6$2OtfvZI?= zU|Lj$q3ay<=GRY}@LfmH(-w^wkzDRBbLhCveh1h>%ysA#zJhFk$fA%poxPISFw~$m z`VE{V1T$K%I++kic?4+wrg8z{mYM~E=-;)IFA_e83*-$E-LR9pp&4-MJbGcOat<9w z(2Nej0K3bEdPEP?Qy{hEQuCEiOa92!RWDc=4`Pf7yKWXkeMHcm-mvxUpyxc`~sbp_mNp z7}fk4i;;XDE+<(M`B@;7x7hRT8;kK^zTZv)~b_vZ5{@=UZsC0w@4!% zOYNns8zqAcp5E187 zox-k-e%fT&=#3R98Wd^_pdX1M`pOql$3aNgDZP86*Z0#Z-(g4ON#aTtIv$3D*e*-5 zEM~Erq;bMkY&*|h`R?5Z_D&aH&z9hx_rwfo-UDQ~t2WH-v7~wNGl2L0y)$0tmpN>N zAj>s^7h6naYWs(&Kj`7N0s7b9miGko`+>cA@|`S6WFj3Fx}n4TRK*OF9xz)7PlznC zBm2d>+lOATgP^~U7(!yTthJbRS`$bD7|bm5MOp7lNz`a-BmbKP0N(0KzghLjdWW7e z1nh6UmJ5xhDOn3)xM_u32LSG3J}o;Ze5CE@9;1Ad{YTS1Xl}m$s=r+8EiM-T`8@4h zo6bJG|788#<-S3;r?F=I;642tl!8oOeBIwsvAxVV7a)^%50ETV1H~9J4#=C2&r6afx+>X#$UCo?>C= z8FcQBm!2@KiXV7c7$gd|88>0RojNSdWXjSo2_iQRmF=fbn^t8H(1l}f3NKddjH~g* z`t!9H_#zr5?&dDck@Nsrq%rfA7r?(Vh2thzpk%C6tTN%eczh9g$?hgwI4kok>(q!} z#0=@K9;HS!@Akg8HHX6=J&835E5?nD`iGX%^-ZfXZ>!o+an4Y?*IG^2uD0qD`E}`= zyWD(y*3KzYT%+^554lZ|j|mPHN@-VV_vF5(J!9&iFqt=kRFV>oqr4ze+^&7dD4Jt} zZ_ML|I0p`tj&Bp-sMt^-n9%)Lao!nV`kAY%l(?6{zLIZ(v- zn@X)Sf~*_)5ih)y4K!{RZogO!AEbHivUMPMZsy~$YFdn8oEI<+-1sn1N(!A$i8vE5 zg~giA!i)TlHzb+``!EVea*oN*u`N0g#4sN<`c+gVK1c!&MJ?*)^t~K}dm1?*RZvh~ z)dv*+q$ob+&BSDVSGA$v;2^9{$PfAI>dO3wq1^dqW3WFU;+UaYzG(}dj*ycBz{&U+ z77e47ar|mCZKdluaTH>fQR!sxuh3)Mj|79$Njh9e;fVD4CxQMReh&j=^*vl49wOPG zjTTqP7PG<~;Ntgzi|c=s`vMo;%|p*(!QJrn9{@`bch!KAF<{Vgf39D@C}r zsts+;LGBhKbCYz}f1G|^Vranf$WgYTbXgtET4lAoxELhPGHh}1>D1T6vDVHpu~;*@ zQKueoa;8N>2>p;If`HGv)NydyhZX?H2m1C^YZ8883yjSii76T#8aK(&A(raF`|UMv`Y-U z){Ibqq-tn)n5S?D)*!k&)W|+lD|lA3OmieNLAgOoI_Ne{lftdve?B!$HqIB=N}McF z<)whtft+qVQ^~wb)P^HUZPSft7TkaWtsinXgbie0*%MjAEY1Uaf zmZls2O@g(pnhba%QZMu!mT5*WDHyi_fTVsJOWWmPB%(vU=v~YgZTk-sQ)vFE-*Ioi zqjig_tpA1Xi*yid9t9W0VdQ~$#dzuj%nv}SiX#!TSj0}u6Ygc) zd-2l2E)rAxcKXhGZ8F{y|DHsjk9PpN-)KjOArRhzq|l+op~SGnLV@)e9}R4qkrHmV zWkk1E@i}){mzVJuHqFPF_3qi2ipR<9s@> zRSbVd?#K!0x8u@<1TVz;N{Cnfe06f-{Q2t5tx8uEet&6io-XH|<9i}?y^z^S5HdgW zUB)9jWU=F?f^%DB0e|i}@9syyAIzt_O^=zU2ER`7fIj@a-mt7wlB3x#T%aOm5GA%f zJ$bSxdU9{~vGDpI>%L&&XbX%Gewg`+L&1KOFy9Lm$PiA-f-p!p4{a{fgY$N@izIFT z^CRkt32)wjZd=xoBjPpcaYY{0MB>n4-PfPaENIJ-_1Bt(<;r#Xp{UQ@l}?&?B5r_FxZJ`01%6&nS-sE^lWMxc*fmbx z>6@#GZ)k~8g%6z@_>Fv%Cir13@Q2cPo5QRe-K+k!@(h;+OpqK|q^B1}HOHW?%e=12 zDJ>w<*e|Kgsr`SPe%Ai^G`-l(l9nBg7UE54BECfsv0HFmULgIHo;7Gizd}|mu!DyB zhlvlb;aK(vv-=fY=Pu zHg>_4xa(t~=lx871PY5|f0Yt`4b_l7yb$|;u&y5{IclFu?ub)%`(c+5BLz(iCjMZB8P>E>fSap*(!#d_VlCE@H&pupx0=C0JSw20!Ce0xw*&{K~0 z9uzoIoIo5MEP`d^dexODLJQ)L{2$Ntkw^>x-iG*MewbJo0ByOD=ROq29Z` zE0lLRpYrmuDrlIXsU3s4-nw)ka1T#A*QzPt!zIU}dy4Er1qO~l^!1=XS60>qoWCFn zHccgRB5E{6^@Bl+rW+z$hcI+glbK0(cF>T6sPw}`r5njawkmIUekPS4CoJ((IE@3_ zW;_Zr44iU;NMwl{CI@-rAa86(-uUi8!0~@gy7iIsN|#NDYJPOQ%r;+fidJ64ZiwR}sW4hDFWDGW_yD!|W;+*c_rEXSHHOt6d0NROs(8r0i#NspkRedb-V@s-t}&JlnWX!(Gb9@}p^ z)ZBI~>`UP*8gBiz^d6R!)*vIJG0&(pOiJ2&OZi~Jh_qNs5h7if%u%Pjso^4@x09;Q z|B06ki=UY$hzO_J#Y0>}RaE3{Rsu|?Br5eh!V*%>|d z;AyYE<}j+Oi_3CV&`yA(a;_)U&~+6Lmuv*o73Ipjx#Rm>7OHHY2uGnLw4JtNbFLDVcLZ zl=$oT_kNLV$W7pTp6y|t8b9_D<^y74shheiwmqKOp&bRWJTR$l5!Ajt{d9vwbrs58 z#i6?POzlfzCOg;|(27vlK^Uh@`Dk&J$Zr&f9+N>Pvyf+ra{#oly@P=Eccb)2V7m@z z=lTomIL)LY#1Jg+|F>Z6l#2J~{GyPZ#zHipjZfLWCmCJe)RW9fMHc7jpKL;Pr>g^htm&WQ?jka+2+GpI5A#~9)W6{@cY zH;g5whj`ZSUC{BO&1Cb#HWGO4h?%rw-XfrTpiI|}VcM{v!Dj58-^(9AsmdU8RexxHIvNCYxtU{DVkEL$K@EtEichFhs~JqdRpL`G339d zQ&;ef;u>0}!vf4LE)YY}tKAzT>(}Z^5nI=)POWP5kjK|7!zs!%HQC>2-iOP4O<4{< z>i!E^&3c{DwOt zcgLb^h>r_cC!N11nNRPiX>&`S^C*~ntH=TPy>-v*WgpE}_HR|HPaDnrj3&Ty9E$UW zu(6ZK`BqWif|#$au14m#8j0$7JkO;97=4V@nqRP_pp^D_R;zRllg6e6gWbLvP1MqKzpsi))v{+@8!tg`d&#N2+`Q)gp+@u!c= zzfb!w;6cQ_{!H!*;&!5q5w{5LY+J)}3 z!zzoUq?r9+kic}RFj;EeAUOJf@gViAs)y-HhtU>CiyW*JEK%rWe&jv}_iMOtZJ0>4 zGem9xjUG^RmFmG;hfoZkD)p1F7;1La6kAbW8%H>Rb?8NP+KruZjFLAR7u(9U8-WYgiBP zQTI*mCS1U7pdA_e9?WO(7Obcv=yEzAkKq*3+VSP|3x`I>+9gcWD1y ziROSfw}d!-Kj5D4DrS2b9JQ{5Q#K4j_!&kjb*17mI3Ug+hB&`93FrFLxG!+i+Z=9& zJWNIE`79N-WPZpMOKm%0iFA1oCo)Loizl#;onZIjrg6Lb5gOiwezt3j(FGkX)UiFZ z96-u!$vEFDkd8WRbhIJr#JpYzP$(pGtdLS4ktQG%M4Q)IFf(PjN(jzU5WKhPtLJn~-(m}+i)B?kz#350T@*pYS+ut0<%^SR7eA|i)n!p`9Q z@Vxj*895ISYC8~W{b}462<7{mYwRMKN*-rS2+RSL2#@jD5lpy_8!0zQGT(Xe5Na1m z-9P0>%~55o^%Lxi(t2Nx5#^j29U7XRtur0YX>>^IJAIZtKL{SfpU$Wz?Vh;0`XlR# zFZ6iO26D5njfM+e``f1v*6R-Nd|Kr1!JgN1{F5{+Z>(mXY9*fmG+nshP`5_fwq{_@ z?ZQ|W8mt+=M*YsZq%)Xzb!k{v15FgveD!n@hlz9?8$?|eXlmkwvoy)z$n!FrJCPl6q2S*1lO_Z3dz}Gz zOMz#>69*!>(fxr)U+Q^-o8`~lE@J(T$r##vi$Q46h-VOX=-xN^?>otROVjQirv3Ru zo;g1*^6XoMko7m{eGx*U&GooJkcCm`yG(ManD4lP@lb&T7je%P(v1W8;`uV;a2FGi z8d&t9{JYLXq={GV5)ZzM1Y!mT#2>;jWJm6jM0H?L%M?oqmu^TAZ3PezR^uYc5;@uy zaIwV`7sIfW-7B5z?xFI2EAwbgze9;T9H!>J;;r1ijkQwgwNqqQ7N}hSbp*<8B z>>#)wIo{fHx5rfx1kXTB@bAqy+ilF&`$q7BZ&70K5A%MI6TXL>u%8SSUb1-xT07x! z8buLH562q#9{k#(aXop{c|9N8!30>F|#zfIBilI~J z2!ZijH4+J4s)Z!+EK)D+6-IxQqJGITK<3iEzo6Ur&$ydym6pstD2Kln=v)88+!N5}Z-K?Zi^Bklc32t% zPWEG8u{cv1vm@oEQ5r^Ja&YpE;@ty%pNUCJv&3HJ+&cUJ&%V6>gxuTJ{1Fsn1Hgys z-{sN{Y%b=4I_Qf-|o#cgypKxsu$865TimeI&&3dwlJegAtRBF(MZs$TOb9gqujf zh%RTTpDCt7$5p;xX2@@|3_^2fR@4}ju;231g* z3$g*gX;@>eYaEiwcBxBlzOf*}D)_v_y@Oz_NjlT(HnG$tj#}hQ!fYai)tT+rB)~mj zzTHq1F1ZlD6?OHS^xO_P;_sCAK=POBYu=ttc-dA{SP@H?^(tSW#wF1Cn%A@LY1vTg z7eB`fq&l8yDz@%7#tUKr-S2#+mEPU&G9|rb|5FYBbOwJnli`oyE3odG1yaUEZ2ta7 z*Msgi;o$Da6}j+qKWo*$)`ja&AVUec=)Q2 zY5(W6Y5UO>P5*){zjl9Si>tZ9_|CHho!@Oa;zZKTd<@!k9Ie^%LIlrge3(s1w9dq?%|J z<2=FPYzD>gX1Y|kXcj{z7XwDyYCq%E73@2d#;dz>wOA}y)4U(LtK}Edylp65R9`U6 zw{?qPCH;-WiInt}# z*Qv)zJnjlNkAuYXG!oDH-{ihXJifa*^@SZ|k?p3;c9AzE_EQ!+B4vT^+sc-<65)Z{ z7!$AVs>^h*tsx=ei-9}qdq_+C<~h<%>Ex6xQc_Wxjxi?r1YA^44KZ*E=y^zS)k#9g zLu?e@YqB~l=LucprB^+E>viVnk&!pE0^4ZO9pnSn(fyj&V!2r(>!)iAfP*SB%DgEn z)tEH)3!b-|SuL!$AfEx2N-^<cJcPi3B)NNp<4fmYaqG!_(nNA)GLaBA*3GmM}kbC7fAF%%mF$A^pHflsG8Q zcSw02MBnCB`vl7KAUfoI`%X}D{m*h=pk%PQS8WRCL*@&gIkt3Q@WPm-%6C~B1c8?Z z(sRS`r9;WkOP)jY9kWhrIwqBbzs)CQy)Wgg${*o!akBtS;B893Rrknx2XO3yz~8O+ za)FGwn$J}ql@zxeMAu29U8b3Cm@Um*A}detUX7d}ueD136X7G0ghjptuX@=Lvo70~ZOsCQeLW z^$qxCb9cP- z<_QTs5MDJWS0~-;Z*Nv`JV5PFF6#ieCPqvHV@5ARSA?7Ae0GtYHYF*{+#KM%4 z)wGM^(KGfRS>R^w5rhz+4>*jXkj)?-JJi)Y}n#~XO$h+fEPXopMG*gT_smpxV z$yg%&h`BD00v={=;yRBy)F=6Pa)`irDkYJh*!L@mK9rL@&D)Mo@i(<)RS`7cpep)K zs-pG3(LDijPULM2caXuB+q(u}!woJ3yAG2`ls2XgLi#^MOxVkis+ zpL}~}r(1OTr7o#UW||{!{g`749z89|$-&FRdMUN#jc{)SoTy(|N~OA{ zHFQveCFi_gWDBjTqDwYODL?z_QfS?{)X9}@8!{J#>1?1n00~=dMpL5QHBaYS%LP5k zTg;`U8XCV`+e%A?lf>&B;PJv2c4VDlIVI}AiVlAhZN4Gpr^H%?8f4`}3#my=4`_4mrcgeYdApCiRIf`x-kG>uwA1F&U!!WK%CwP#E0iKu zt3p?h-M&sSA1`YvOqpou$hMMrtXNwaXJ)u_j^!%(X1(P00EK3#KyF%CRu}4e1Wh^y zcmUG^-!A-3UvP75K66Gt+2Da2m}@DOKm(#4fvYZkEQ8NM=d{I3L1-WA5jdv;&#A!k zOJ_8b*&!>(AAmphnl(LIk%H1-2Ul#uh=qd^>g+2dK@mF!G%swG-j07mphK z9aR7Q^5xWeP4p9AP>lvF3>7kMB`P>L-u5}XtED88Yhy~%;^Bv82ooE99+`whh&+0i zFRaJl6>QszSn8Xt(IMcg`v86Wa{9)aAlDCOJmNYNJ>V?FQpPt8u^ND#98nb(>?E<> zP+)kM!@bLDPbVqnfzMTG0J5B;+Of2*VeR&#8NzFK4jrF@)(*+$me-aJ}dAJOO%kewAgFIlU; zwu@>g>*|q}XBKU7Ld{@ZX^er8Wdc8Mtrs7{fH00pBiJJrIw5bGMq87Sb4RTpfwcwA2|<>BfpfoE$NHaOvN zA87jYqlRg~fE*3HQMrvUU5B#WG2nx=XZmGY7K=B#p`8MTS&|&?Di2qOoNF%xJ)m7a zF_c8C3%~`KK#T(6ut!)Eu2^^bX8>#QKXkT__;P6!kDx$cQ9VYUSn3d#mW z<&##-ZZ4z|-FWTVI90Brr3SrlM)~aIwPTOyf5$OC;6(1o3F)`v(uD*s#QIA3rsq=_ z*07=&I2OOXRJ`)%tCJJw&sR?vM-)d&gkBP|gbTodksY#BWHxhEn!2uSyP5AjbN^4r zxlh6W&bV{qKkqc<$##_dj`PHxpf@IFWzWct_&aK7POG}zo-3&D?LM)tW8WV?w$O`h zX~ONNp78ko2_JUJ`YWDTZwr#x$S&N0#4Rp@ct4Ch-j5U3jsE&Uaee)5dSAqKfAee{ zt`Lc%Kt2zm#AAMvCM->Yj75%c1CFlNhin{Cu#2H(rzo$VlE*DbQ2LKAbC8V-#OSw0 z_oo!qjTp-=I2C?S85Fc3ZzCp zy_%p}r5rY;lMZVImlVw_w?siY6B-enex+|yiotk!DRPP1rSbV1jy+BY)z``KW{^4| z{va{_IEfKtHt*?#G3nZY=O+R3fkw=CFmT0_8D^1+ec$%OR2_t|gD|!YVeDWudlU{| z{|nt093I3QcDHjuDCoUAP$@}T=Rk}z-Hc|Pm%4F8BW9fIHI|ZdN~r-*v_&2}5CT;LFmK?ob<|NNF#N}mAs}?z^vhf6Um%`SWsLvofDT12;|lbS~J1^C}<)%exd&fTBsJ@Q}9ty3h^WI6T|Jk z?d%wh(lSwR`6A=noJ!Uy!nLi32FL)*E0uO&RgcKuu$5g(5OO3#_Q0OIf@mtRKC(C&(xBNK<_k-jyIbZuT50KKMQ^Ec>4Z?GYNZQ>+qsJ6H!uQF&v zVf|yyL2keVv^48&osY+4#lhq#^sSHJAU5n7X6CeHlh)Ohrr)M>7Uh>_J|pxF9RdMS z!he4M?V}MBO?Rb5OdAa>{RSE`vfk?XznWCmW!|6(mXm>!h?RAoMx2tHR;m>Z)$0j| zcVVh(OieJ2I3h#Yh*~vPr6RK_HPqBc!U^GAy+huvu&e2M(XB;~uqHj%x1n2%{$z5BaZku4d+F8Hwt6JO|PASe3_Et@&kq2aad8Li)d!O$!DaaWk5<)8f@;QjZg< zFb;x*r7pT6C4LOBz)o2fqcctFIgT4Xq15v|;jdX}=iOnpeX=uWV<`OThX9>ka~a4#Sp45ou&o~FnYmZAIl1Rpf&NkgEvZer&%<8o|~pLuf^G8v(s#;8ee z;EohPSxahf%pwK|0d>biW$F|uigX@3gz`5irn^KaMI|oaK*YFM^k!PshUbhi7P{_p z8lUqH;hihd95BX~Foy4Wwi~%AOClFerc{KCdxEpf2|PEA9Tf=ofHA%s##rH_eiL@A zfEVje;=aHOZ*zDdQ{QuAl`v^15%Wcwveb_hi(E91hbnag_r=2tZwE6D$iKmm%D&Wp#&zQ21|xzAr@{e(%1toM|k%sOj%O`&Z_)qh5y z_|eJO-j+IX5(e&7nf2+6YC0n0-0JF&tSjDNkl&&Wy2Oqpw#(MrWfq|Ca8@h$x3OAx zF?rfgP^%m1$>t{S2Nb`s9~(>Xx~e}6FkKqIAt_x~OV?-#=&Zoh!P=7){V(BeS(U6y z4%ZA?eaIs4wA>kHIo^W(OPSsV8L@7sz`BtmYmoWs%`PVYg`L>3Me4C2^I@wN31?}N z1uXC~n>&#mapgXDNn`&A>KylhECyfdd4s#<&)x1~{Z0wbbJc7OjzS|2LdSO%I-c*e zA2OmnN(xzjcitB%WDDGcPVCv94A7Cn&X}M2lEu=ESj64H4Sa_?GJf$=NZ>phDdhLY z5LI+hx#}CeX16SXa7rnbDr^m`eQ1JQ6@}D^90>ZB0^xW$Ov;8nr}GkUZ7vjXvSARF z_Rc+s9eXTxyq@9NN(73n2|S08IPijNs+jY8`uty4jv<+x0Br16bbdNXB(j0Chw~0jQlWv{qpG}F$k^= zJ(r6hjz8=5NKB@0C|aaPYdx2j2L4yX(Bj%&%Qm=diYuf|lZ%>CRQ#Vn{O%+SC zBw@ZE3dU21Ggk`Ri9Ijz)A-;;vpecd*L|1-;O-rGGgJw6KLcg<{>c!}Zq5(>G>^-& zv-V!RFOo#!Z!AgJv6sl$bC^(p%Y3D5mMEc^YYWF$!gp={!qX+jBYzit&%4mJPV4%6 zLu}*uC2V73OpB~8b&%N;M!l^;0io#@5aEAqt>!9kk=1*yBl#%&!Y;P!>W+eD(Z{6b-#G;*Bi2LW2kwX?jb3YonD(&v;1NWa>Q2BW6>Q8P_L zzH+O`FS-`;+VE80(taDB1p=vB(U zl=t~%0In3(c#O^fWtY50bHExk?_+pu@5$`PTkDe7*N71cbPCXn1H@KUitF_<{oPFT zXNYZYc@}8=D51#uT0^UYv%z=M0uNU2LKBRJffMKHbg?RnLpO;7=4Fn@d^dC%595#p znHNN9nkK1u_I}3Ez1le>>$oFH^kejL(No*@AJEI0)Yo*H9FtsoUk@K^>OUyxzL$b- z{SS0cFrx2ouAoaJKMGynV`=21FnBH|n&1&rcI*kj*FoZ|14i7zfVZ;+k3)wOdy)u( zIwsof!y=??d&Ci@LhVUP)pi-?`Utl{C|j>u>ypwL{Tk+`p}Y#S3N7`EH84zy;>oq)ousy_hUUxR|K&QW7t;RUXEg0 z^~kiDwEVsYZpX&0da9n6vM4}UIM*o-ZrFDy;+FvF6^0@ z-lMy@vTyWc*%VcEF`uox2?u)18<2Z$MEgB8xQ-K$IGqsVhY zW}}z1?W!y$E1*_7tCQo^KAu&6Co1SP*y-^KDnMG?Xq2Whw!z3+1Trr3WyE!4A3| zze6WBgvlLleUGpS6KpgaSuac`4D?D7SVdnx!7_APv6>d}QnTR0sIpdJ9=Vc`vfda8 zC6_mNFw`ZNYLTrO;Ty)4)zvjR%K*a21w5mZ{_0$C>$}4oF!=T`_?irMx54_Ax?Jeo zhj)2BZcapTOS^s#D>uDDtDezPhn4<>S%y8&NmE7pAZ2NCSZwov#cpKFG;&oM9uWWT zY1n^R%c{idcB9E-Ti{8}|zOf($X2jKUefZzHb>7D>T$4)j^%mz*-eLG=^ zmxauCl*hOYAX-L3#meS93hWo3XxQ1oM8j_>IBe+6@(aZXQNAGSw3^Lzq}3CInVrGm z3mbsWS&LCQ-Qf(EOg>!PP^W+^@>!uwlO`3WIMZMdaVET$6-p7cvx(3$Sh7~=e?nPN zhbS@AgN8a|4f(7lM}%lRlNEBW;dfmU-P@u`UG6su4?_;NN(eg5%vHpf(Bwk((Q~Nq zU66E8m{vg`Wy;IzK}U24)>J>g#l*Ihd>1CWz;Ne|Is+3MmrAFGgLd|5dIz*1+=2$R z6Kr>lS$k{P#^47IUIv(APZPftxPZ=ix8l#u+@8bmozrX_62NUS5-jwiOvH}Mq9h5? z&2nYD2~=@pQVnIFvkxQ zUpQ&(+Ap3?Hgb0`zw9Y~u}w>gBjoY=cUaG=us&7znc{!)%$Tk>QH~)S7I9u(=;Eik zcvaU`-G{{^e$X1RW#`?2>+Ybjn`WL}gk(u|nL~lHMwTya-A2sQE*rgb)VfK82o-h5 zD%2P%K^V2%D+=r~p=3)Xq}MAwdo^U*4Rbi9QR7XqP4k<1BOrlOQy#fG&6wX?Xhb+(Xrzk+ukvqR}4f0_N#g26ksZI zZ1&N7H%xz)On;UQ`}pslHiRs(BSa8qaK_o8%lw49EakD!0wItL+;fC)KX*Nycb}ea zu<>o6qZ??jsKzJegK~oV)pSd{x8B{}e{In|fG6hGsG*$TpsL$GMqTAWx%OAOH<)yD zMV;bdl4fbb+$;l3nnnQw6dy9>WWvjwOof?z@tD-xK@ZBa3c%my)C6xWT~HTFmM`hr z_&~`vGEep=mtc*Y)3^^>CNhE~_jtO{gr zW`ArGPN$gXp{{0SH6tKBpN%zfKD2r|1AI)MSi{ z8%eD{SNnpbyv>tEaXXD-fMm?IL&^Ls@fc4%pK*|QU;t&xGw;PCsa;H2`{B2D)@usS zFI8*(2F=3pgt}o<>YiR3ZfQd#s`a!5vgyL%32Go5s*_e@qJs}v=71FhTC6Eylb1o5ye_Qji$&jK_TTAIJ+KJ!!GubJSN7*z^y)CRz{wG zeVh#)v3+n28es=HeDT>~^bO)N1utl}PHU;Uyu7T6OOryZD?op+owK1KvF}<&uxo&{ z?kvx+Bhb4Vwk>e>@co~Z-~>;#)|3_rj&@F}SWC?&0Dut+!=R7Vk;b01-mpd5(2eQA zVD@2)Kqga|Isjg{og6_;s9|obtBFD!{`u_}25V&jC1JW}1*)nk#e;x9%hu?nC*0t1 zPOa)1M9n}vDyqB`+V>Jhq0|i%yrDK?UplaAR;l_bin+wRXoRQ>HRj^l;)@_Lt%JTX z7DqI_t??X1CZR<-HBPfSu-QXlifczw&5i~uHOq2g0>qGa4VU?c(_Z;5E9g6-H~``B zvme}lX$Hf%8*m!G={-Ty-9|wh2~P7#D`v(hu}B+1H(rNf7~!`VhDoG#)%yE^076)ji8M-Kj2zCN7IS|O z)wSc?r@DS;NT%_hclvnRj+?3DJh7Xp|8PD7%}M<`?zg8^y+!|0{~6@4aP0fz$#9pg z`SwD}o_@mP`zL(ZCF`$v4tM!W(fHee)GaQ8ct0!`-j9>FO{$}ZCGNF%>V1*8{mpF` zY2x{zFJi|12&prIjB)7*<_YEbK_s|HUVL;}w2SGPzq~u8bIXct+uovZy;SV2+{FX2 z5RC*N=3VA>RZdC%Mo#91oP*i%P>KanrM1SYM3D;FBqrnsi=dE1lv#!4%De_izPMf? zk7w3(H75bBGpckYFZl0>qeK;QKjNJwblFg+&Eq$FJ+MRV)Na zl=VqsYo1Js{0>4SReHY+d57@W_5a%>$MuEA#o#TAkET_ksOEBMEgq3ub}le`2xfA; zW62(^=mQP;AE%!i(nrv`>=N>6JptC9lj)9nr7Q1pnd?WmAP0BMoKja!v%I7avh5F( zZD~{6bBXp&Pb@o@fsB;PBHzvcCrLQ;mE=tMPUgFz2ZNkFb5E>;IJ+C->_L)wlq9qM z7rHN!Ot3kRogMiwd~U+x%!9#8d4lP6LdH>siDk-prVf(KE=C3*^wvVhcd`*8=+iHt z<;$d?GY?=K05wQ^Fx`|!5c&}!MzlDil%$mn!vcbmW2yY*-B`pMk-fASo{F|WvgUJ7s+Mx0F6$mt)h)Ujc&4N80 z&a$eg#@E_vNdc}%=AJ_vm!_eUTe_Amx9_3W^cLpDJmrG?>rCFpen^a91qHT>k((Y! z4;ijUkTp6v3)ZFrF$8l3VkF1-!I9SEq1W10t*v(mSU*B=dQ!{n?up#PRdJ9Rf1Ewk zv11#TwCA`a%*F8g<=RELv{j&@fJqrA%#Y!(SlXP)ObRD<<21;;gI)9>NFD^qA1Fw! z|B>#CAQ`$F3zEJAJ3R~_^M&s)U%C;9l(xf=bSLtKqteJf2$IR}36j74d}_V^O?y-R zteT6qn)R0bH%}l^-jE!7(XvmuZgPvFOo?slWQki+CEmPcYQxALo!#$da`ei$j273u z(`d)TNW$WfYp0?(@;Su}0aTXos$Q8536aJ`#*1mypv;Is7c-!D=D5y2ccaNi2$XZO zcbTvovfc4|taLJaiEkEF-s+C%a~$Oo>;Ot=DC*`EYp)?c#WKO^!DI!0T`GKXP018T z(a0OUl!OuuF#JD-uXgDb9q=D=L$|5S2eoi*;b z=4sslkSDRgK?TDByMP0^?6L8@?7G>-WYHH!j>j3F=0(nFw#>vVxT>rQcf4MnT9jkj zy<0(BFH@@Wyf}HWmN*rJN#aSD!7(bB@4)$*#J*sO5=ojSzMaWnuPt#O%-;W&wv#=P zo!;vc&pMr_MK0Jc+Sz1~ljY!I`lK$V_QA#UAWuJyJiY#gy*Kjo<}Rjw%)`)Cg1M=R zVeo>KB_Kz$G!=0c1)=M!17%yhgY+1Ge12!W{v)qjv>KUD#d=pw0UpB1_NEglewa*+ z?dxRfTWuN9?SoXksZ@=lxI(Iio+x->*>hJDooriNHuG3ue>&RV33T=B>{sj`3bI)C zcM@jtfrBi2kYyLLto8r>zg7?%$I@Fj{~x^R*!@blTi$)Npn9v~^@Zt;W!=cTL)Hx< z+ZJi;Fq!#2^Fz)V4`jqVFN%^l3p|xRjecsgMj;;1l;v^={*r}y)AH(hk=ZF?1apXLe6|Y$zbpT zmt{hwp|I`L_S1ua8$Uk*_s5!yhvMxmlkq{geL~^(mdSW$1zYPN*d7GiA1~OxL`PVU zOFt2z!y+yP^IhSv6izu7d9IS&%>tfg2S?b0XuBoR_8{0iO0Ze~E8QExW^+1LCzU)4 zeV6&%bu>9l;zd5o!Y~LEp7@b)U%X(G?4V%ty9xFBn@X+U5NtQ?6C|5_+h=9$lC)WW zuTmrHP&icGv6yzTXTSgU(Evmv@GMwFG1Mxp0c}#aT2BW5MF&d}NR)F5$y^o#j)v6v zM+BsyMuz_wQHO}~)zCX*QT#7-izB!9kxm>*LX#R}%pbG}iUWi^#b`lz@(M}vv3)Q; z%H&Z$>LSKqkDy>acnt&&WyCY{ww8xT+QBffo}2_D6*1c}{r{Mk`9GvQd8YBUPQ;sQ|Bg{pGO}oY`BkOP2 ztB#452y9_x%j=GX=Lm&4Y*m9)ueDxeBxx=;35A*)Eh4$FD|z0SY^5tEj;_s1qgXUe zqBTWDQy#CBBnyy=6wZrjhhZw>CWvzo#RtL@@5Cg%F?Hmr%WI5YflIops>J<4uN^Fx zCcYESX?Z~z*EfntE93jB%c?ms=`K=`b%{gGX!WkVDASJXYF^5&hBc*H{ObDDDM~MlRk=wmsHe#>qni0j^sx6Iss=VV$epA~vUn-sKQu{H zznN&MNr4*Zc>@g@S#N1|!AqFA^0kfmnjkxgO{7VkmUcXzm*PE$b zPy4n1pcJzHOzw*m;`p1Ve9eL^3%rEMECx{|Qi{ci@R%FAVP=DD=Y}tyqB6F_9hO2& z993sw`SwIxYl|d6-Kq`P|^vglX|1uqwd zo0W3M&7@j4WEJGe!1NAx$R=<5jA91r>*2iF9BK#j8zF7?f-$A^h&3<&t}PQJt<<{D zgM4M3K!7s9%1mS(kSj2iJzq}Swa8(yFw6|HSzuL|{OssMl2O+3WFyC#$DH8X&|^tVwGmG^Ws6A>ah!0=0rC*;HqmV&+8p3uF>sg404{KJMY$?tbPS|5 z1aP_w)cQ`}+uT{}egU24=E6Ndn@ymN6UC0SgMbAh44Kbm#u6FYEE9Id;hglMG(13? zorgB-PvgEo8-H`YC;|WH`i{#Y3IFmv0EawMDNEhZmbRV3-Rz}9o6w7Pl6aWZ6(2g~ zh{dd*JD|ZV^MzUOOF64xwXf~|d$WKAin&$u$a)7@B~L=H~mahGh6HE*HQMJni%}XCK~wGGjn?vKe}Gk||Yc zo>X`%FEdOQO3hva`aI5eGqjt{y*E`U$1vx6;Jm4)AEDz3ed~kV=Xyx(+Um`*mVp7F`5~Jr8=Vw!h+sgy}Q>V8te6M?>^`V$_ietJM5#w zvx`~|qQ>Yjt~4*4F_yu* z^S08OE9ga4G#ZBWL6c1v!<=3ZVg?8&QeGs?H>?A~Q?+v;%6tx!xF<2n2B44ddpyzK z&O*^;R8<;0!%na^E}E&58C&b9;Q*{LLNhI@@v&<=-mx7V+u^ZZ{R{;V=m=A+tQa&4 z=484dE(#stIl}Q-04FH(L(DG$SC}2SK@g^K!jpY*2YMpq)l*=h71(F}iP{&|S7&pp zm~>+|h%%Qcn@i>g5of8ClEDrYs8D21>>Sc@{-5@~wYzN`+w%MT3QpY*o!mBLf&^cl zPpKqzD}9ryOgU9`XH90g@uJL*M5;+Cj(e^9-{%~Fq^O4BF=BTN0K{V-iwSL56 zkW4FqiG>(`imZ|aX86^Y*L!4NX+r0d)%SNvg{if%542pROU1YT^8=qz{sjy?PLIi{SI7Du&xXrO}W z#GBAogA~_+siu^c$;Og+G_)=ajm#1U5kf0devTt1uvi79me+NHt6_|>sgOOM+AE&I z0oWWg>3ndNeaR+TW!wNJe0Pd7ZA&;L<0zG!Mnw2L0V6r~i3nIgQs0eOC<8z7+#U`c z036zUppFI(#XATRa(&I9-@MR6b#myu*n__kFQjflJy-FxheHP#ByqPiT| zpDW8Vfm+wuNDl#A;F%G|6L)mGTHg(zpb3({!9JQvqa?B*!z}6p9soHO#_-y^)73N6 zBW+~0+bYdT6=IPFn2fGBMI`oi417~FJ|b2w9Uz=JR_fjbVmblPYiN+r!U0-+-uD0$ zqR{nI7)Avs^!&HC72**-w7xW@#cVjl?m#$VJxnOQj;=1`tL+iSUJ*uRC&vz5HtW%c zorGKB4;8Y&RZNqZ$`Gk$^n$-%*9mL*Z{vI8v&v|4(g%=ksxcr2kW`PU3tj(bkr z?ewm2!$p%*4T;atEDl7=DS z9_7SWQKF?%q3f%o7A<|UWVd%Jm%eV+^51b~#hs>?D&4D8Dea|7pCsY2cldxLsKqQGDreSr%2bB;WE5lkXgj?rd0G9n@~rkZ5TjHV!YUvRx@YL%-b;rDl>-GOU+^KT85FP$axgQ zDhuq=Eo5nL=N+P*_reQs7+Glk3F7GGu`8Y29IzPiL}`7W$FLV@?7ZFwt_+FcKA7!AywcR%2AtPilg)5+6-F1@gJI>J+nNi8SOwB<0GKgDt@Y60nn7*i}FZbl+8Ehqw%6_ zA7w1VP`O+)lK7kv#;72iy4ZO>S2|GAi~B66gdUz3<;^eW&Zk+Pz&4d^R81oY=y4f%kW_anei514n@Hf-u4hM^G zSJIW%avE;JV7MSFta4OhaNtU1{5h@eDAaM?++S^?+dG`!n!T;s@zS#N;Wd-mS8ptI zb0?-n8@f;CUDwf)SQ!Htx{+#rH&@eW&GDM|5UGL{x~?)|GvDXA_&4aw*49202NTU) zzFs@^SVU4`DqKw0JZC-A;5G-;Yvnb=u$9*oO7%ji&XW~Ny>%pyMTo-PKF2(ET@)%W$Y7y~XDAhY$uIMJo{3infniQp^eKW0ba}sjWWNsNCwVro*&~!&FIgi5*8ws8znD)+P*o~*nG*C)j z86HCnbTmh72rHC5)v_ds^V@_g3=l2ymZ^C^6w8?MU-K+;l+>_nLEISwWV#$-YC8Zx z_*9qh2JsS}UK}X{vr&n!$KXO-Utq6aG%{U1`||EEQK%S;MG_L3cpgeR2_gb2iHO1| zrJ4uuqQ^uBugtaiFdhpa^0o&=sS=UPy_hh?1OOtP5}pJ;Nm(ddHw~hIvmOw!!vjQr zYSDG1I~@_yhJfeHss1@aD!Jb)3}PJW{DEOUnTboOoi(^IU&7{V^3>5ch2PEcE1sj5 z3ORx0C__I5lGfRz_Bv0!g2e}b&=`|=YcI|GH#U&LwwDh40i_*B6>n4K7kLmHu0O1` zs{>)aJpbSCJ511DXCrheiEIQ=MQ@tT-idZ9v}Jqlgf(FV&r)^vv;yH4ZeXeoIx#=R z`~JJV)4$tWK@v!GtPPx&#SobtZRiHxV@+JBD`Wt$q=glL$g(u7f$yx2hJ6TIGV0t} zK$RVKL|vY=2^H4ShG1|Wl5+zG(P0n*O~&Nki3yyr>&erP2Ek|G!agT8gVX5j^AC1{ zoZ3g>-zw8j&I?3N0G#ZXcnbFN^ADRP+b>*w&xH3ILo^giq>TKOM4X1Og~09+CsLCn zl@awK6~zxECcJlu#xu)%ef9CflEm0LB(*N*b>opb8fEtFBcD|yn8#e`yqGb<5|076 zR1xt~75ZTsd131H%IpV7)cmTEF={<8EBj`iBUpKPn_HfTo`r)O8Wyz)!PQDx(## zSUj*ZuBEQl*RL4Mjqpg$Fks2LbegtweLDGa$tTX+Z&Hu&?*1%FOJxW{Y~(m<9rny- z0R!A@y;C(hJTY)+R^}(R#`cohLwA*1>Grvnm0q;O%buk1a+^J-Wen|3te*@ywJHH?Fn#V|J@5c!~>X7V*F%{<{bMrdz{ zgnu_w)J?B61H3|{ZG&@Q_RZP9EseMdvQFpcqO-}cgf6RYappO_6LDiFV&gBI3%NBO zMY+22^=`y%+=#C563GQ4O5m9i2auKMSQF~Sk&GmzTnD`y@!;Kvn-Al$AY?|jK*(T8 zR%EJ2un|qm>$AnT4*|tXB`l)jsSTXb)kj@^;FA^S6`%5RQ)2bHnB~)zklY{m*CBl9 zE-}c#>bLIG4Ck`@KW>Pg{XvhbKTJN>dwva5e_f6Ly|j?S?-0D-_qThxi?t7!8KaNT z!!V<(KeOrOf9T?;OpjF42DgjEVURu*uo+8-IBsO(>67c#xk zK>hU(KCD@<`2;YYu@;u6uz%+HgT!r=2p{mktevuVJvmF&KIF-%CS0P_e0MwC>`BRn z7E%4tF1(#*#T0Ljxd!3D-MDv}%R41rfCYqkWNHPyfay3YjNSNet}9Z}&HcE^qkIkb z)3TbH+WzOlRas>W#GhctgQ34id}iH|D>vGVhP|Wv4li{FUbxM|*s--POy=Z8$7{() zXKARU6s}96ICKf6nj^!dPof}Ae4WZ54(WrO3b)t0w4hEV@9Jedho3H`n&8@ji|w!Z zIiN;u){%i5x?a%je-i`wYx){${er{fEj_$|*~gPLeH@*6Y~Z=!!=K!{ZuNd+UAH6N z|5($*7;B}!UKn+xyM~&E0S&uVZ_TH`zG95(lX#4X&-faUO-gCgivmhG8k^j91e5oA|8E|A^ z&xU6v&-oz(XRluzA9OvaUuNhZN?bRhN#X@8Vs4VqhxS)p&HcK?I}3t=n{1<8Ik)t- zGV<=ayY=QDo|OK~t8XE`p5aM`&yp=VXqFMIF_UL_oKlSRSuxQ$ zRydhFq^Fu%+j2X%(tfniOR_e}R_D}x_QPoV(=u2GDf4~e=>+4kbV!8IjCj81i#U}Y z55nhtS{4tWhYmN!x;ZPWv$fIV{~hw(ItMN_`w0~AWm-(;XdUKv169>QJ)P{XAK@YRPpu~> zmO~cP*Dx*$6U3UfH?7R#BXbpaih^(P{` zIuW5!OqCl1B#>c<5xM9}PkfR}H|1dx1g`9p%?|%WWbk`oaYZlC8bsPMxel|*vI z35$6|IE?@b`C&*s<)^+!pL@(u=pCS%>FX99IB#a-32uhOdDEa0;?$3r^EOwLB6~nc z^>P7o$g^7Yz%e$s39eYVE|XSY9lx|9lH&qb6F@Md+1P{&;a9M{eQADX$miO}rukw0 z<=byHasDfuFcn}XQHKLL=kMNrs5`+#a7>U1&?Mq3)JZpF@g3FoO|Z_xLWx7S+CG>V z;Mx#I{RkN4?C^hq`#dvGdq-H*&WW@7UhNn5^#B`a2oZ2$J5$Z@n%}%wkR?UPd;t?7 zonDyFac(uGm=7+hnZQymQn;&?pKs3+7x_0}jUb{tl|e`XtrEh*KoRco1eTg0b(16w zb)+AJH7>K^m`sUqJ&$co5E3T|$28v*qj=*%1*_P1XRLDY^IsDnK-P`lCX4Aeit zYT}qha;I$5Zq%zaZwc(~dkVGrgLx#UP?Y*x<2sj0nMR%($E-lt^w11<*Jbcdlb)2kInXi&FnN-kInvi zxjz`jLf=&hLrF%(V8KE@G8rcD*7dZP2P*;^>yJ_MVi`QPqFg-34}7i{5bL% z@uS2iELEHc#XKUl=8C&Q$7%BP1wxO|TyW#$cF<^7TU>lvLflVF3G3SdMrc6~7#|BT zZv2=Yiv!mSw&!Q}m{6L#NJvEqi#?Cg|zTYwl!6bNqaODjEn_?4uXXp{``^~%!lXo*AwJx zM4CmU_(YxvsKH!(Zu1B~Kvfv1_ai{hFU||NqnB;YC4kIXZc;8QM?fPqpj0D&Qzun+ z>PObhksbm>9K+8~YBKY6EjR3^hWa|IE^Pn-0#*RIxgK@JZ|b{;@=sAoy;}ydi3)A& zZzwNM8Vb_Ni0gEi&(Wz}%L;b-w)l#rkE|y1sK>nD8}t6MGFRpyuQdR!e^Nhj4Fc{; z-Q$cngBfGKK(^Ed)J|~EYCr8>l1wm-uhaG{WD$#zL2rW~31Q_=S#n3%^KXGi{$bjuj932|cW8Fu>x|=_)M}u{@4?>L+ z?dkx)t%?H=Q)V^cTEs+inx@oubr=QD9_vO2s73VNxFXN%2T?hnc=ZLquJJoon};Yk zRa?>&OX^Bb8xC`a*HH}D#xvO<#6|gOKy7-C*-mgnKg9$C0D<@^Z1>q$fWX6znK|CK z*mbF)E!bQNUae`%+l3yv+Z+D#!%)@f9i*!cQW#q^s5kgX32HA4jZ>c3@is4w#wPr0 zM3hd?&ENB1p3Z-H3cuWAI4e)3)B#r{3>0icT6rW6DCS+diLZPvlY~F@IEv#sNp}PP zcD%T_zV6Ny8u&dtd?a|d`BQl;@X+_RhKDR6ZsQ0YV*zG+uKj;}jTf=bMnYsZCxvT*U@^0#VO8VZ0<%Ux z34MVjU>=J+JHfLEJguHkM+B+WJ2L6AeI)ErI1Lw2VLL^Bcn~QYo|EfqJ&?utR6tjs zTJ%JaSs%qwPd%4Lr*3d6uZ1p&t|piaKLkT~GWER;xY=%WC~~GH^<%KFg8)|1z!M}+ z1SgtNUwEEYOvFdvQ1ouYhHo21wH+ZtZ_hE7(H)+|<&|{HV9KcX7d{#w-1uoc7C^|h zKO6V`F!WOql2i-y7c!4<7a(<%av`+z!<6c04+!G}t_6>+YxKxxIW9MiCr~dP`K+t1C~b~aqoi5cO#V5@9CK`Kk0+m=U(w3IS|IsC z|1;MLK^@u~Mp_l-RP!bsO-HJ1-i<3RO#$uu$xJkFCO9uAP;C7k`W%py!nZG1@#~9r zt`GjK6)*>{i}56b1*|E7my`D8RFuV+`B!3yv`p>0Y+7+O&hq91soG{qSPXqPUaI_C`?7)ub^gW-N!wLLY0?zi_wy3cc{RfcKx`P><~_!y9gkbT>0Sa4 zY}LLil#p0=I{}a3vX0|ni~H6=+B1%&YkUb(4DXy69EP+rWa?5(Qc z;H|1!zy6WTs*VX|E56$};#F11%mhjp3#Y;**lE|`#xdU;I#1isDeC#K97fTbBfUA& zn}3VH9W*N|qoqxF)fiCM1bu;suG4+|#`|(wif_Ir2Tsk{ zxpM-nFeSyb05_u94P)Qi0oWu!iFPLyIe6D#Mhdu5) z9Ng!|!R@&3w|W-ukmw@lao!{0yv?7+W5Idh_Q`+D4``?YN!--+2=ii266$%x4JhSq zp8G-s0YcwKSr=nPill>@=f z@y@{cRa8jU0&QvQfo?yF21Tl9l^~c~x67PHSL|PN3 zyLfQAN7bu<1!?Mq#Fq&}Zbp{~<@@k9N|S*5+)ZWgK-(|IforFi=| z#u&;?LK=~Xx)BC}CM5Cwh)5NNTyQD03ZFgjqru@G9KO%;**E7;Eu2?12D%&X@w@=& zbo~i7o9eO{VM~KW0I@44;A+WGc|Jc%gyta66UwCmQY-B-qHcDbf%E53Ti=7kvhkP5rCQ{p>z44!)&Tfm zh_`I$`->|8S{yDalyy)Tr-btXTG2S5aPc(cs#ysoq4y~REDG?q)KGSgj;GPU`462p z1X)5$ZdC)csT=MMBcQN6Xj*k*zbN&@%n>M~ZAGgIR-wNHg$e4D)t>>zz)hd5xyz~m zC*w@ug|N%%27;}i0YgZ}BLWLswE>01ZKuwuP%#^rbwojwsF%e`v=PsJf;U+LiAMQuo8cmw@a`B@N@9%S1x#1ul z3${$y*4Q$PVirUwrYd4en4&2WB8o|nL~hIjNkzb(J+}1y!^M`3Gp9BG9pZe_ZX1MpZ5J(jOKvxN-jOuC)AUDJoKh7}i#>TY+ zE*NEBwDUKef5{N)pvw{Zteke*T%(J>pH9bAP;zcq|7i0N3$kYGEn%sY$yq zXt6Zu3kShw%4Y!mjx5Fv5e-bKi*{hnaG+KL-D*xUwa3%LG=XI^J%3fH(O!znp(p*O5~;PKQ~iMP@~Ty_Qes9)>0?y^S?D39;R)6f`btgVpQZ zSj~(+WR_=B8>x<4#mOQKdM*D*@IM&pX<3X;EZq$5$fczsY8@EHF`~b5Q~*ME7N+T1 zrFV%@Rh5H3ZsVi)JX=>o^F}5eaH3JUr*neY5E-3dkJP@i`p~n&xR?d64RjV#X!FMJ zQ+wIa3upaebx+hfLXCF~`h<^Q?y2hre4RL%tp@5)O}z$Hbqkw!twBFcaWh0j7_7CX z_c~ff8^&!lE+3RwvKgFK!l`ljAP6+`0&bx)lV@aTNhgTZiWEB)ns5mpnq*1%RG0AP zs!p7}#l6!eVYCy>)Kf?~E1Q~5a73#yia0{q0BWZhdX#Xgtt}GnvdT~;vYswBwOf?M zl@WZhheoIiP|YSreF+*3NJZIvQfv*B1-Di~-2>;9on=>-h4VFo?yC%$>lI!!GiOi6 z{o0AcH)DHjuB-_vxEknsfrE0P^6J7&+~otx-hK6O_f^C~tt68~2Jjb4WJr>TQxXOg zU_(q3#^T<6^=R&^Wnb0D_8GVn-fljUM*?qsx_!WXOqFyaFC;RisN)`K68mvV5;yUJ zfCVx0pS`kV?9#*cVIgE|jv;?8s_7n}ZpX`IFzwuJWN?HkOqQH!*2o|*r?BT|Uo(YG zHA;|ck4M{+Py{#&2W?fYgLcxOG6P#X*wz_qR94BVan;p=v`46U1=~mo#hCUQAZv~< z&B3Z^pp-^2*|eA&nRE=-s<~(i6Z^O7(JL%Y#QS*~$nPnmAp?QxF}M)d7uf3;_O!1* z&f6UxVqqG)EDeb8;)F0?g@ng3C5azLG7S=zMnR8<4*WQ8^FcfoDCC9Pb4<80q>;x+ zO7S?)ivoZ`iNH{?G>KWt(lqU%&;fExylR3zmcURGj{mWk%&hL-9zfB#QS7K4MN6=# zgBQlIS!9!uuH~@NZI;(@SWY_9dg{zQBDH%DV8~1waMclzG34dgVa}vM8L-JC4g%0r zj17+-ILJk%AyKHAtAa2kfpj%8HoR+4Ew#e!Wc*TOO>N1;H|4BoyilypYVJpl*US-?1n$*_kGd@}E=P@P0*Oom;7sH3F%F-3pZpl;m}nAztZqyojWG` zueryC(%TTO1ulza+2bZ;YeRR?r{%U~)oKc4OaR14^dcwPT#R4L zJ7M6wo8sGl&N7q6XM4tWNLICOr)gng(bkP|N14yY(3|dn0!;rLM!5U&a=O-!#R@vz zi;iAbr{X@cdJAFq&VBhoHkXY?ndrfkkKhS{?R7NDhIoNdkj7>86hI&PztF}a;(EfX zil7Nb#+CVa19w>D-+2F35YGZp0E<10OqY&J|`&YHD6rO`O=0}|lf(bBSPc)0BJ+Q9xMhuln zsfQS(SUQ7W-hOgW(5%SOeEL=VL(8f2p-^C4y?FiWhnJ{=r^jLh#SENRo%COfl0@_7 zmX<+9fQY|cF9uN>N*H`%XTwWOKdu-1<`!(Ldpg1IYuUW$3?mp9ok4!NPP}5Lw#7q2 z1f$eVeCCrFsScPB0f~ixb;6a2qH!9y)a@PhhXeuX9Xb;?EtlJ(KKcZ}{xMp5PiVY* zl>Z=U4XEv{X_1?L$ecT)SO_ya+u3l!FeX?0u0DLb@c^5-41}4?T z1jeM!s=BF)TMO$<)14FvkI+*EphOB*(E;+_DUte@Od)Y5wG6nVO(>7XNKL7zt*DouuMclBUm}mK3Yf3e> zM$(!>s_b8|MdugddTLtgx$y(n&FT^+adq>Z(L1{oxBQ`9epWhdJX*UVU0~Ss#k!tY zUqv%o=ILLz$U}cmb3I+27(Wb)m5=_xzw*@&D)_JXIxK|-E;hJ*!ylToq6{x{bmSqR~0_JXBB2&fK_gDP*Mn`0H8B4!Tf;x2!60H=5i882MJ^2Gb{8`7(HWyUkdf<>C=My1RC8G8lux1{ zO?;io06u=0QF3d&OJ>SsGCyDFI?BgcK7;-1*Zf?TTATHBz&t;s;rtKlI^t(zq03)W zqqk=kiS9y*yp~fwo~*U%e0Sg`P2T2%#`pFczOQw;(2j?nA<-c9v`!-u2A%@bTqZDVsMa~DycF5g5YpP#SLU9%$&=Ko z6MiTFE0mj(+MjDVD-FY#wl!mmVXwoCR(q^b*+@DJ!5g&vd{(spZV(k2k>vxGF>9XL zh3G!)v%t^N0nKYb#=F6^*IzYeCY@9oF8dAd@jj!<`9<&cg^TYbf zx8Lkj>b%0K0rnFYjkMYMySE?eJI6`oOiKAR=}|63pqkNH*MTN<_9MK zr!LNXa8blv9)cOv-4;=-Sr*qrip@vtC?G{NNa*&iP?0hk28mA?!zd*w1mT>)+aPvXlz2M! zdq{DJna{uRyeh_ziYn$+_Q0YC7KZ>V%63NH3r{Tne2*|9mV%|B7mz5z)f6+7xTf%z zN<5(xl}1AKFyfG4#O5P*B%TR3-kyBU^E}N(#7Ph-%*u0J6xj4gpk3&`zzgBa9!9VO zhY=r3vn=ijM@-t%dmzyRiN^;L_53?fByl5|hxTQl0x7_}RRLk18Jq=qJ9w3T% zqet2V5BC^7N(@wu(CjBZGZg-Y+zvShxI(t!1 z5%Z0mKzj@|3StT!%*Bi!ccz7z!I?w z-`ab6U@VH;$D%w|EXw8&=8<4Ex`TrwKx3Q>}lm?ozltnRn_B>2s zbbxY{Z*y6wI>h{qu3zI~X5-3VLti4j+_us5HDZ%c98wrHa82ii?+EHv+PpIMd0VRy z249ZEmn|QY6{O7O@jSbMwd7U`5SXz-zQ50RYT)B1NeYLhVn8p|~(Mlh2hggJqfBP0$ACDj{AHbvX*Cp9 z6EbTe&ueD{#g@?eaq(3f1qGBXxXMN-(BR}nR$V(;I@dc;sDFv55}i#|kN=)1{;Ma} z8b_fa5y9zYXPnU};Mr(a>UPfU(RHZfAfhTr2@Dq{ER-He(jX*B5-aT~U#leOvlI_V zaO-YfN3$d~rt0pLwvL6^@d<#s%^%8R0d@ZNZbPkwh&2sK0N9SO6cBODV?{jSdlBG0 zN)_!vU37pcY?T##`d}$9R+%`q0(f=lT~ZA|1)`WCv%m@^LDeDGsDYerFD4|{a*xjKRk#1({jflC;Nzu>=wgo&3hNtuqBI06S>FWl1$_dHqTxA`-A zEXWTt;I>45Dv07ldLfZX91#}Kgz&_d#N`Q1m2gAusb`P;;sZP|{#fC-DkmFHiW2&LY9vf~WfoKC8m6?P^RHQ1wx$7jFqYkoFw?+XR(~5IQz7j73kUwr4uE&b-_ab=n@I4$;7-G9g2^n)kBbQa$>*zfD5{zJXg?`X{9e$0Eb zVf#4KjhEu1F&nneES9Mo`5qWqajGR@5%UPAzK^VNu1{mFqd@j%!vRJCoZC#A^GiJ% z+0fE8r(XB78=64S_HZ925ON@lu8|}eoxJD&=ls*#4>mTlgUdN$_elvDe58lkfl~RX zHV-BU#b>zz=M6m@%E)UlG;B1dVMNA2QFJq#dYlSx#+F*Q1+n;$XL-|t3BvZu5bXE9 z;-?o40MikRut5P$Jn?d9fTdQJW7V)KPl_uTE}R>NS9y<4V%PH&ixlw_84;F-3E`ga z!7J2`j=0aIIszxLe;d^Mh+98AnEBGC`EZoy?-}IV~p14FP=^=BBByj|AFN(s5dZ`;Iw+{$l2N)3WN)-UJe$M^@tB|vP zQNAC#v$QLfbA`-S`LI5N$2)LR69{kItxXIjpG^ylh_~=>#Ai7!1$dNi8w9w`e%QmW z$A(`xT7aOYmuD|N|M2?k^JjosLzn<{KEsP|arpA{XERLjk1PuT8`hJu=|O*gaCnYdiVl+;DXM)ci{hvuk%~ zkg*;OJ`x(-{5d@qH0W;60OYx;aD6UF6iP-Imo7<|rbM7<1k)-}LHgX$V0?%Q2X&SY zdRNAi>5hX18duA$!2qv1k(@Rrz$AUQ7vrgVaRqRTE1S1%BRv~FAajz=o1CHFe|x`S zw7@IO0D|qQB~K zMBuVWgejG=_w20}KR!IGr3ox}U0zS7#jB6+obyRhq==mVn2n$lxWIgkYv;x5Ki(31 zTcJxG;=G=fU-gfCG_ivCZi&onz|R;CD4?yhL>r% zRwh5r0A2jJQ!?5V_%Zx+9tAyj=$|wu9Mr=n6U{AyJ35Zxeyhd^o7%jY6KAA{U~i4F zVEc6RPW|r_Qvue>*G%aW43buFoYb?Eyqc+OO5h2pEsrtSHE?6_+yA@p*!;h}#yf{W z5J{MoDuHc_i8v-;`)VRXD*V(Jfs%gDvU+%8zu{emh0@NsU1)YF_!}Mvu%u5Q_;usw z@>qanxIM7+eNT9)_DPt!0b$e=Bne_h0-m_37R;6E*;8P};Q;HlFU# z*I}R~?0i1D1_I%dIQ101@|l-VC6|H(dRTe@u+*dR9f?`pg&Yq(zg_PkNei0s1QPR( z>7e5&4}9 z%7^N|s;#6S#?`lKbWi4vd(pSe3*3mnZEk7UmR3ER+rilm^Pzn%)_{mZo@zHDu1aFU zB0naA28zTgVquUbL7Mh_zJ~+{y*pC({_Fg`b8Z%xCa-jterl+`_ftO-4BY(DJQ^4n zZO`o%a_t6DLL%-#=f$BHA8eK24 z^BV8B1}tcjwR+x#13-0M_Jr=>uETd5kTZ1i?dGv0O_E`~z<>UHeqH_f>z^U}{CPZ< z%SnUXUrqJc_d37YBjEF?U^1c!Nj<>`qiINZA{0rbPJHQUo`&922Yl?-q`j7We^!qL0s7n1I0&BwLQqD+5M8M%7DTAf zB{5GFjRKc5;Xivv;6bwc0vrI?X1EiT4IHTHb74(Gd$tY&ZGsOTOx}f>sXWV(6t`P$ zs?CXh9SeXiL80fg4hH0L=Zn6^V)^UO51!&>@)RHng?X^TG%S zD-zZQ)!MLyiDeOh=9HcKkj+a^q2Q%dIpp_+Wm*;R0^0;&$y8}#S#!J$OSf?qJ^uVY zF{)@4oY5QUyzCtp`3-bld@9NU%#S8oL1oos#IGwDZFxBlVHoBxm72HkM3{8X=6z9p zXi2t9|hl2fvRoc#vta`OEW1qi&?6ZSx@^Xfl&JY`0c93iA2Q`!0x@W63V+0Zz&%qoo8nB}u-#(N5pM=*PE z<2Nr}EV7HhV}T|}OILgYcild16RkuL`6;1kB47gyDTyUbh>WBk`$Ett_4+XAJ`B1K zgYLtik1!1S37mdxzCs_1i4txvQX-;6uqYN#Fn0-K+yfIO62w;?3t0qJMEbd(enhT& zcqYo?;VrgkOR7Rx(DWF?jlm+Ycj)B$${7{X((09sb~XIJDgZ3PvQs&#$h8S(Zew8J z@+`*!05jMF6a0wR=8JJq7h|6FA)(-2;r~f^Jc~ z99Cy9%qf_`{il}P68=#y_Xy`H=LwA|CoJ(`vyd)q7J`N(4TL6skVwK*5RxQ;zc`HpCbi3i zpY#y!0KLqA6eGAH=lqhxLt+Rhoi}=(%8}+E|kx<*_Pv)_pwrG3Q#{HOv5tOZYk^*Xzf+VR3 zh|dF+x}nSc)|35wQJd!1`G_ol-I?e(x@9ub`SBnV@dr%|AV z^!AA^d%WTN@IK3D-|p6`-FT#q1Z{XbNOPAgVM>Q7QLYc2r&5k0Zwb8hSi+@_x#-ab zJwSD`-)#(;$(M*wQh{DXXxgQY#C~P=zdqo5`=?H$$Fh z|1w9T2JKw6?Y7xd*l5el)JUgxfyYEkjLCHHK|=95XBGFhc{XUuP| z);I~TE};u2Fj7cpG{XF{I=2j&Q=QImVoK5434nmt?2lL}%FtbM>;eg0Q!Wm#>6f~O z)MPd_(W#SJIVmda>aqcm`}t>_O-wf!&wUJ_D!?N%oB6Zm?{TQ(&z-hG;HSN zsO}y-2)`^U)BW)Dp-Xf_RQ!8`e(Tv$cRn^=US6<+ ziV`J-;oCa+6`u?2m?=ywSiE3qFn!<{VLwj>R#kurS{R9-2vgM(MTtCy_z z^@{bkLfB1ZgfsfWu7NN!F0ex{a3Wl2Ssf&fUMcE6c=+n-YEW-X16iC7XPMHcIzPo~ z>vv!g;W_;2q|{;t<3`3K0_%D;xSWnhKWxbf=Cep7G$COaq=a!UiAZ!z!q`W_SkGs! z+~ZmLV?yV8qFpq5Zi~{-WVKs~BAtv}($tdEyX#g&9>Zi(iTc3q)?8jR!po5-sr>=6~yav4T#W?9! zexE^4H!51zXEH8u;Q(+lnvHXdk^p&R`IqKr_#6+~XQcUI{pH(l_Ca-C;p_l#$A!&_ z(a!n1w;$@8H>$f6*kUcVZ&4zYf|%iv#0o;U+TODU;B^Q?d^88Sn7DLi9_@}F&CZFl z`d;l9_VoZ82n!z{;3{_3x@T>O-I5p1OvelJp%Fl+i!dKtR5O7^T%>SIi@)9DQLpE^ zzEm#Fo~mt z8nSi;u!|w(q@*E{n#x$mzR<$$k;4H7ZM|vy7tpxrvOPK}q$_lg`jlvjD{!n8;oOZ0 ztipa6Cy8Ql+B+!@1#E0ST1Nvm!tIYYyh!*mN{EN93FcEy0A#pCc|jr=ho05X{&*v# z@gW~?d@Rj+xF>WmX-n@9IC@0!_=uvOe+PHMaig!oi$gz6wICvmBEpzbVEgibB)(1) zCL+&W%69L472o%aW8?8U68I3u+j9%KZWM>i_lTQ@9>9mxBSJ}!DC(vnBH`x? zA3XOd`6?a@PW0Z29$OqTwm3L<#f^wVCfbj6$cUdZNtj4c;E=_{)gk4ft2s@1k2ns< zVX^Ue9Sh<}=nj&qbr^(P5Sl>0F)vJrV4+Kd)*h9S>$_6*h{HQP#PO#V&Z`cI`1w44 zr6Kb^bAIn;j(pJYQf-VmUOA;kstA*&aRs$@smn_}ot4NA%N^zhm}ERDka)wUF*TVZ zmDOWfx>jk43zta~sdHj2#HgziTJoBH-TC>`r;pCq+(45$#Ydy!3a(CTlmd0zS@-OcPJ7d~uWI7;le%QAyiMEbWHv1%|FmgN zhmqku8Iyk}Q>00<3&qpNRG4%!Xda0HJlfM)bzvyn*%E!ty>eG{KDf%hWD`IU+|an- zyHk8JF0i~ULqY((TCYEqMDRE!S_dxm5*~7$_Mm5ndEKu*eptdg>p;-DV33VR z>S*8&+Zyg9ItoMTM?^9n09JGZ5(lwIq{m{#W64slKcDe}Ckl5y{(An&`K{g(-_-Ep z0f5Jpb6?csx2Wf2=dj1lq2?=;!r=?xr^h-y);WBvn1EG@+@86Ws%$LjXdXkJOO>p~T(Z@1q%yMWlV=d0GRC zOejgB6rf}%d=a{SDE()jO%o>G;nLfDG-?6l^<_3vfO)KMyt>RL+wdPZ!g;M=b~(!} zSI2ytcC1c8gI)BT{}|=%+MRgJD_mSG>s`xuT;ZlS?C_-ESwXL*{bZ?+Yt0HvP-m9Y ze_ta?SB>FS!;O5C(v{mT^vJ6V76BZ3tTQ> zQE7bR5=Irj$1Qamx?IA0Y{2jD?(UYQLb`2V?Hc69$>8l)P3v_2?M?eoeVDw>hjU5w zdF5-GHYtix_4V#P@7}_4AyZy`xnJ9si@9}1HN{@-xLmZ0?lo0ksH^eX5#HmGsgDf2 zO2x!YFhQ5cK8a&VNk9Y2W0$2q75fFvy*l|8>g0)kJMnn4G5lPh+hLi^XVW`d5GGIj zo_zd~$j3K-NRLJ`W~(g1xu@%5RspEH=s0fp=#09N>!r#iQ5;f2spdp*<&!8#G3HSQ z@bQBzsoU#avau$UTKhqNy+A_j1=DGqf6%ZZx2@;d*$;*xqr|_Z#)4cI;!+VZ<3vlLHy`Wz%-ufJZEH zY1saWZX0lR?*R{zK@o@sI(ZWdtm*GLvU^kceSGga+l)vk%3;veAg21_Rf5 z@qtTN6^rWfe;Jy247ZfdujkHxfhBiA*+ux$98~B(wfN6$dK&oAzz>}l|M~N$5ARQ` z=K6nV`K5U2yoU91tWRTi;12vGhAA8R&N)wcnbns)WCq={2CJh Date: Mon, 4 May 2020 10:07:23 -0700 Subject: [PATCH 011/143] [Canvas] Updates function reference docs (#64741) --- .../canvas/canvas-function-reference.asciidoc | 271 +++++++++++++++++- 1 file changed, 256 insertions(+), 15 deletions(-) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 16aaf55802b17..657e3ec8b8bb1 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -42,10 +42,10 @@ filters | metric "Average uptime" metricFont={ font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" - color={ - if {all {gte 0} {lt 0.8}} then="red" else="green" - } - align="center" lHeight=48 + color={ + if {all {gte 0} {lt 0.8}} then="red" else="green" + } + align="center" lHeight=48 } | render ---- @@ -324,12 +324,14 @@ case if={lte 50} then="green" ---- math "random()" | progress shape="gauge" label={formatnumber "0%"} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" - color={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} - default="red" - }} + font={ + font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" + color={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + } + } valueColor={ switch {case if={lte 0.5} then="green"} {case if={all {gt 0.5} {lte 0.75}} then="orange"} @@ -693,7 +695,25 @@ Alias: `value` [[demodata_fn]] === `demodata` -A mock data set that includes project CI times with usernames, countries, and run phases. +A sample data set that includes project CI times with usernames, countries, and run phases. + +*Expression syntax* +[source,js] +---- +demodata +demodata "ci" +demodata type="shirts" +---- + +*Code example* +[source,text] +---- +filters +| demodata +| table +| render +---- +`demodata` is a mock data set that you can use to start playing around in Canvas. *Accepts:* `filter` @@ -837,6 +857,28 @@ Alias: `value` Query Elasticsearch for the number of hits matching the specified query. +*Expression syntax* +[source,js] +---- +escount index="logstash-*" +escount "currency:"EUR"" index="kibana_sample_data_ecommerce" +escount query="response:404" index="kibana_sample_data_logs" +---- + +*Code example* +[source,text] +---- +filters +| escount "Cancelled:true" index="kibana_sample_data_flights" +| math "value" +| progress shape="semicircle" + label={formatnumber 0,0} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} + max={filters | escount index="kibana_sample_data_flights"} +| render +---- +The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights. + *Accepts:* `filter` [cols="3*^<"] @@ -867,6 +909,34 @@ Default: `_all` Query Elasticsearch for raw documents. Specify the fields you want to retrieve, especially if you are asking for a lot of rows. +*Expression syntax* +[source,js] +---- +esdocs index="logstash-*" +esdocs "currency:"EUR"" index="kibana_sample_data_ecommerce" +esdocs query="response:404" index="kibana_sample_data_logs" +esdocs index="kibana_sample_data_flights" count=100 +esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc" +---- + +*Code example* +[source,text] +---- +filters +| esdocs index="kibana_sample_data_ecommerce" + fields="customer_gender, taxful_total_price, order_date" + sort="order_date, asc" + count=10000 +| mapColumn "order_date" + fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} +| alterColumn "order_date" type="date" +| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" +| plot defaultStyle={seriesStyle lines=3} + palette={palette "#7ECAE3" "#003A4D" gradient=true} +| render +---- +This retrieves the first 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order, and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields. + *Accepts:* `filter` [cols="3*^<"] @@ -915,6 +985,23 @@ Default: `_all` Queries Elasticsearch using Elasticsearch SQL. +*Expression syntax* +[source,js] +---- +essql query="SELECT * FROM "logstash*"" +essql "SELECT * FROM "apm*"" count=10000 +---- + +*Code example* +[source,text] +---- +filters +| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM "kibana_sample_data_flights"" +| table +| render +---- +This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the "kibana_sample_data_flights" index. + *Accepts:* `filter` [cols="3*^<"] @@ -1107,7 +1194,7 @@ Default: `false` [[font_fn]] === `font` -Creates a font style. +Create a font style. *Expression syntax* [source,js] @@ -1244,7 +1331,7 @@ Alias: `format` [[formatnumber_fn]] === `formatnumber` -Formats a number into a formatted number string using the <>. +Formats a number into a formatted number string using the Numeral pattern. *Expression syntax* [source,js] @@ -1276,7 +1363,7 @@ The `formatnumber` subexpression receives the same `context` as the `progress` f Alias: `format` |`string` -|A <> string. For example, `"0.0a"` or `"0%"`. +|A Numeral pattern format string. For example, `"0.0a"` or `"0%"`. |=== *Returns:* `string` @@ -1559,6 +1646,34 @@ Alias: `value` [[m_fns]] == M +[float] +[[mapCenter_fn]] +=== `mapCenter` + +Returns an object with the center coordinates and zoom level of the map. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`lat` *** +|`number` +|Latitude for the center of the map + +|`lon` *** +|`number` +|Longitude for the center of the map + +|`zoom` *** +|`number` +|Zoom level of the map +|=== + +*Returns:* `mapCenter` + + [float] [[mapColumn_fn]] === `mapColumn` @@ -1612,6 +1727,12 @@ Default: `""` |The CSS font properties for the content. For example, "font-family" or "font-weight". Default: `${font}` + +|`openLinksInNewTab` +|`boolean` +|A true or false value for opening links in a new tab. The default value is `false`. Setting to `true` opens all links in a new tab. + +Default: `false` |=== *Returns:* `render` @@ -1675,7 +1796,7 @@ Default: `${font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" colo Alias: `format` |`string` -|A <> string. For example, `"0.0a"` or `"0%"`. +|A Numeral pattern format string. For example, `"0.0a"` or `"0%"`. |=== *Returns:* `render` @@ -2184,6 +2305,102 @@ Returns the number of rows. Pairs with <> to get the count of unique col [[s_fns]] == S +[float] +[[savedLens_fn]] +=== `savedLens` + +Returns an embeddable for a saved Lens visualization object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`id` +|`string` +|The ID of the saved Lens visualization object + +|`timerange` +|`timerange` +|The timerange of data that should be included + +|`title` +|`string` +|The title for the Lens visualization object +|=== + +*Returns:* `embeddable` + + +[float] +[[savedMap_fn]] +=== `savedMap` + +Returns an embeddable for a saved map object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`center` +|`mapCenter` +|The center and zoom level the map should have + +|`hideLayer` † +|`string` +|The IDs of map layers that should be hidden + +|`id` +|`string` +|The ID of the saved map object + +|`timerange` +|`timerange` +|The timerange of data that should be included + +|`title` +|`string` +|The title for the map +|=== + +*Returns:* `embeddable` + + +[float] +[[savedVisualization_fn]] +=== `savedVisualization` + +Returns an embeddable for a saved visualization object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`colors` † +|`seriesStyle` +|Defines the color to use for a specific series + +|`hideLegend` +|`boolean` +|Specifies the option to hide the legend + +|`id` +|`string` +|The ID of the saved visualization object + +|`timerange` +|`timerange` +|The timerange of data that should be included +|=== + +*Returns:* `embeddable` + + [float] [[seriesStyle_fn]] === `seriesStyle` @@ -2579,6 +2796,30 @@ Default: `"now"` *Returns:* `datatable` +[float] +[[timerange_fn]] +=== `timerange` + +An object that represents a span of time. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`from` *** +|`string` +|The start of the time range + +|`to` *** +|`string` +|The end of the time range +|=== + +*Returns:* `timerange` + + [float] [[to_fn]] === `to` From f62df99ae38801b905dd10d281dbf972c5b17578 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 4 May 2020 19:09:53 +0200 Subject: [PATCH 012/143] [ML] Embeddable Anomaly Swimlane (#64056) * [ML] embeddables setup * [ML] fix initialization * [ML] ts refactoring * [ML] refactor time_buckets.js * [ML] async services * [ML] extract job_selector_flyout.tsx * [ML] fetch overall swimlane data * [ML] import explorer styles * [ML] revert package_globs.ts * [ML] refactor with container, services with DI * [ML] resize throttle, fetch based on chart width * [ML] swimlane embeddable setup * [ML] explorer service * [ML] chart_tooltip_service * [ML] fix types * [ML] overall type for single job with no influencers * [ML] improve anomaly_swimlane_initializer ux * [ML] fix services initialization, unsubscribe on destroy * [ML] support custom time range * [ML] add tooltip * [ML] rollback initGetSwimlaneBucketInterval * [ML] new tooltip service * [ML] MlTooltipComponent with render props, fix warning * [ML] fix typo in the filename * [ML] remove redundant time range output * [ML] fix time_buckets.test.js jest tests * [ML] fix explorer chart tests * [ML] swimlane tests * [ML] store job ids instead of complete job objects * [ML] swimlane limit input * [ML] memo tooltip component, loading indicator * [ML] scrollable content * [ML] support query and filters * [ML] handle query syntax errors * [ML] rename anomaly_swimlane_service * [ML] introduce constants * [ML] edit panel title during setup * [ML] withTimeRangeSelector * [ML] rename explorer_service * [ML] getJobs$ method with one API call * [ML] fix groups selection * [ML] swimlane input resolver hook * [ML] useSwimlaneInputResolver tests * [ML] factory test * [ML] container test * [ML] set wrapper * [ML] tooltip tests * [ML] fix displayScore * [ML] label colors * [ML] support edit mode * [ML] call super render Co-authored-by: Elastic Machine --- x-pack/plugins/ml/kibana.json | 4 +- .../chart_tooltip/chart_tooltip.tsx | 182 +++++----- .../chart_tooltip/chart_tooltip_service.d.ts | 42 --- .../chart_tooltip/chart_tooltip_service.js | 37 -- .../chart_tooltip_service.test.ts | 63 +++- .../chart_tooltip/chart_tooltip_service.ts | 73 ++++ .../components/chart_tooltip/index.ts | 4 +- .../components/job_selector/job_selector.tsx | 280 ++-------------- .../job_selector_badge/{index.js => index.ts} | 0 ...lector_badge.js => job_selector_badge.tsx} | 35 +- .../job_selector/job_selector_flyout.tsx | 289 ++++++++++++++++ .../job_selector_table/job_selector_table.js | 2 +- .../{index.js => index.ts} | 0 ..._badges.js => new_selection_id_badges.tsx} | 32 +- .../datavisualizer/index_based/page.tsx | 4 +- ...s.snap => explorer_swimlane.test.tsx.snap} | 0 .../application/explorer/_explorer.scss | 316 +++++++++--------- .../public/application/explorer/explorer.js | 63 ++-- .../explorer_chart_distribution.js | 12 +- .../explorer_chart_distribution.test.js | 34 +- .../explorer_chart_single_metric.js | 14 +- .../explorer_chart_single_metric.test.js | 34 +- .../explorer_charts_container.js | 109 +++--- .../explorer_charts_container.test.js | 12 +- .../explorer/explorer_constants.ts | 10 +- .../explorer/explorer_dashboard_service.ts | 7 +- ...ane.test.js => explorer_swimlane.test.tsx} | 50 ++- ...orer_swimlane.js => explorer_swimlane.tsx} | 253 ++++++++------ .../application/explorer/explorer_utils.d.ts | 10 +- .../application/explorer/explorer_utils.js | 4 +- .../components/charts/common/settings.ts | 4 +- .../jobs/new_job/pages/new_job/page.tsx | 4 +- .../services/anomaly_detector_service.ts | 58 ++++ .../application/services/explorer_service.ts | 308 +++++++++++++++++ .../application/services/http_service.ts | 104 +++++- .../services/results_service/index.ts | 2 + .../results_service/results_service.d.ts | 11 +- .../timeseries_chart/timeseries_chart.d.ts | 2 + .../timeseries_chart/timeseries_chart.js | 23 +- .../timeseries_chart_annotations.ts | 8 +- .../timeseriesexplorer/timeseriesexplorer.js | 30 +- .../timeseriesexplorer_utils.js | 6 +- .../public/application/util/chart_utils.d.ts | 7 + .../ml/public/application/util/date_utils.ts | 2 +- .../public/application/util/time_buckets.d.ts | 37 +- .../public/application/util/time_buckets.js | 26 +- .../application/util/time_buckets.test.js | 28 +- .../anomaly_swimlane_embeddable.tsx | 115 +++++++ ...omaly_swimlane_embeddable_factory.test.tsx | 50 +++ .../anomaly_swimlane_embeddable_factory.ts | 81 +++++ .../anomaly_swimlane_initializer.tsx | 201 +++++++++++ .../anomaly_swimlane_setup_flyout.tsx | 95 ++++++ .../explorer_swimlane_container.test.tsx | 124 +++++++ .../explorer_swimlane_container.tsx | 122 +++++++ .../embeddables/anomaly_swimlane/index.ts | 8 + .../swimlane_input_resolver.test.ts | 271 +++++++++++++++ .../swimlane_input_resolver.ts | 211 ++++++++++++ x-pack/plugins/ml/public/embeddables/index.ts | 23 ++ x-pack/plugins/ml/public/plugin.ts | 13 + .../ui_actions/edit_swimlane_panel_action.tsx | 65 ++++ x-pack/plugins/ml/public/ui_actions/index.ts | 30 ++ 61 files changed, 3100 insertions(+), 944 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts delete mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js create mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts rename x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/{index.js => index.ts} (100%) rename x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/{job_selector_badge.js => job_selector_badge.tsx} (68%) create mode 100644 x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx rename x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/{index.js => index.ts} (100%) rename x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/{new_selection_id_badges.js => new_selection_id_badges.tsx} (80%) rename x-pack/plugins/ml/public/application/explorer/__snapshots__/{explorer_swimlane.test.js.snap => explorer_swimlane.test.tsx.snap} (100%) rename x-pack/plugins/ml/public/application/explorer/{explorer_swimlane.test.js => explorer_swimlane.test.tsx} (70%) rename x-pack/plugins/ml/public/application/explorer/{explorer_swimlane.js => explorer_swimlane.tsx} (79%) create mode 100644 x-pack/plugins/ml/public/application/services/anomaly_detector_service.ts create mode 100644 x-pack/plugins/ml/public/application/services/explorer_service.ts create mode 100644 x-pack/plugins/ml/public/application/util/chart_utils.d.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts create mode 100644 x-pack/plugins/ml/public/embeddables/index.ts create mode 100644 x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx create mode 100644 x-pack/plugins/ml/public/ui_actions/index.ts diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 038f61b3a33b7..e9d4aff3484b1 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -13,7 +13,9 @@ "home", "licensing", "usageCollection", - "share" + "share", + "embeddable", + "uiActions" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index 9cc42a4df2f66..decd1275fe884 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -4,56 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; -import React, { useRef, FC } from 'react'; +import TooltipTrigger from 'react-popper-tooltip'; import { TooltipValueFormatter } from '@elastic/charts'; -import useObservable from 'react-use/lib/useObservable'; -import { chartTooltip$, ChartTooltipState, ChartTooltipValue } from './chart_tooltip_service'; +import './_index.scss'; -type RefValue = HTMLElement | null; - -function useRefWithCallback(chartTooltipState?: ChartTooltipState) { - const ref = useRef(null); - - return (node: RefValue) => { - ref.current = node; - - if ( - node !== null && - node.parentElement !== null && - chartTooltipState !== undefined && - chartTooltipState.isTooltipVisible - ) { - const parentBounding = node.parentElement.getBoundingClientRect(); - - const { targetPosition, offset } = chartTooltipState; - - const contentWidth = document.body.clientWidth - parentBounding.left; - const tooltipWidth = node.clientWidth; - - let left = targetPosition.left + offset.x - parentBounding.left; - if (left + tooltipWidth > contentWidth) { - // the tooltip is hanging off the side of the page, - // so move it to the other side of the target - left = left - (tooltipWidth + offset.x); - } - - const top = targetPosition.top + offset.y - parentBounding.top; - - if ( - chartTooltipState.tooltipPosition.left !== left || - chartTooltipState.tooltipPosition.top !== top - ) { - // render the tooltip with adjusted position. - chartTooltip$.next({ - ...chartTooltipState, - tooltipPosition: { left, top }, - }); - } - } - }; -} +import { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; +import { ChartTooltipService, ChartTooltipValue, TooltipData } from './chart_tooltip_service'; const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => { if (!headerData) { @@ -63,48 +22,101 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo return formatter ? formatter(headerData) : headerData.label; }; -export const ChartTooltip: FC = () => { - const chartTooltipState = useObservable(chartTooltip$); - const chartTooltipElement = useRefWithCallback(chartTooltipState); +const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) => { + const [tooltipData, setData] = useState([]); + const refCallback = useRef(); - if (chartTooltipState === undefined || !chartTooltipState.isTooltipVisible) { - return

; - } + useEffect(() => { + const subscription = service.tooltipState$.subscribe(tooltipState => { + if (refCallback.current) { + // update trigger + refCallback.current(tooltipState.target); + } + setData(tooltipState.tooltipData); + }); + return () => { + subscription.unsubscribe(); + }; + }, []); + + const triggerCallback = useCallback( + (({ triggerRef }) => { + // obtain the reference to the trigger setter callback + // to update the target based on changes from the service. + refCallback.current = triggerRef; + // actual trigger is resolved by the service, hence don't render + return null; + }) as TooltipTriggerProps['children'], + [] + ); + + const tooltipCallback = useCallback( + (({ tooltipRef, getTooltipProps }) => { + return ( +
+ {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( +
{renderHeader(tooltipData[0])}
+ )} + {tooltipData.length > 1 && ( +
+ {tooltipData + .slice(1) + .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { + const classes = classNames('mlChartTooltip__item', { + /* eslint @typescript-eslint/camelcase:0 */ + echTooltip__rowHighlighted: isHighlighted, + }); + return ( +
+ {label} + {value} +
+ ); + })} +
+ )} +
+ ); + }) as TooltipTriggerProps['tooltip'], + [tooltipData] + ); - const { tooltipData, tooltipHeaderFormatter, tooltipPosition } = chartTooltipState; - const transform = `translate(${tooltipPosition.left}px, ${tooltipPosition.top}px)`; + const isTooltipShown = tooltipData.length > 0; return ( -
- {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( -
- {renderHeader(tooltipData[0], tooltipHeaderFormatter)} -
- )} - {tooltipData.length > 1 && ( -
- {tooltipData - .slice(1) - .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { - const classes = classNames('mlChartTooltip__item', { - /* eslint @typescript-eslint/camelcase:0 */ - echTooltip__rowHighlighted: isHighlighted, - }); - return ( -
- {label} - {value} -
- ); - })} -
- )} -
+ + {triggerCallback} + + ); +}); + +interface MlTooltipComponentProps { + children: (tooltipService: ChartTooltipService) => React.ReactElement; +} + +export const MlTooltipComponent: FC = ({ children }) => { + const service = useMemo(() => new ChartTooltipService(), []); + + return ( + <> + + {children(service)} + ); }; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts deleted file mode 100644 index e6b0b6b4270bd..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts +++ /dev/null @@ -1,42 +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 { BehaviorSubject } from 'rxjs'; - -import { TooltipValue, TooltipValueFormatter } from '@elastic/charts'; - -export declare const getChartTooltipDefaultState: () => ChartTooltipState; - -export interface ChartTooltipValue extends TooltipValue { - skipHeader?: boolean; -} - -interface ChartTooltipState { - isTooltipVisible: boolean; - offset: ToolTipOffset; - targetPosition: ClientRect; - tooltipData: ChartTooltipValue[]; - tooltipHeaderFormatter?: TooltipValueFormatter; - tooltipPosition: { left: number; top: number }; -} - -export declare const chartTooltip$: BehaviorSubject; - -interface ToolTipOffset { - x: number; - y: number; -} - -interface MlChartTooltipService { - show: ( - tooltipData: ChartTooltipValue[], - target?: HTMLElement | null, - offset?: ToolTipOffset - ) => void; - hide: () => void; -} - -export declare const mlChartTooltipService: MlChartTooltipService; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js deleted file mode 100644 index 59cf98e5ffd71..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BehaviorSubject } from 'rxjs'; - -export const getChartTooltipDefaultState = () => ({ - isTooltipVisible: false, - tooltipData: [], - offset: { x: 0, y: 0 }, - targetPosition: { left: 0, top: 0 }, - tooltipPosition: { left: 0, top: 0 }, -}); - -export const chartTooltip$ = new BehaviorSubject(getChartTooltipDefaultState()); - -export const mlChartTooltipService = { - show: (tooltipData, target, offset = { x: 0, y: 0 }) => { - if (typeof target !== 'undefined' && target !== null) { - chartTooltip$.next({ - ...chartTooltip$.getValue(), - isTooltipVisible: true, - offset, - targetPosition: target.getBoundingClientRect(), - tooltipData, - }); - } - }, - hide: () => { - chartTooltip$.next({ - ...getChartTooltipDefaultState(), - isTooltipVisible: false, - }); - }, -}; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts index aa1dbf92b0677..231854cd264c2 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts @@ -4,18 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getChartTooltipDefaultState, mlChartTooltipService } from './chart_tooltip_service'; +import { + ChartTooltipService, + getChartTooltipDefaultState, + TooltipData, +} from './chart_tooltip_service'; -describe('ML - mlChartTooltipService', () => { - it('service API duck typing', () => { - expect(typeof mlChartTooltipService).toBe('object'); - expect(typeof mlChartTooltipService.show).toBe('function'); - expect(typeof mlChartTooltipService.hide).toBe('function'); +describe('ChartTooltipService', () => { + let service: ChartTooltipService; + + beforeEach(() => { + service = new ChartTooltipService(); + }); + + test('should update the tooltip state on show and hide', () => { + const spy = jest.fn(); + + service.tooltipState$.subscribe(spy); + + expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState()); + + const update = [ + { + label: 'new tooltip', + }, + ] as TooltipData; + const mockEl = document.createElement('div'); + + service.show(update, mockEl); + + expect(spy).toHaveBeenCalledWith({ + isTooltipVisible: true, + tooltipData: update, + offset: { x: 0, y: 0 }, + target: mockEl, + }); + + service.hide(); + + expect(spy).toHaveBeenCalledWith({ + isTooltipVisible: false, + tooltipData: ([] as unknown) as TooltipData, + offset: { x: 0, y: 0 }, + target: null, + }); }); - it('should fail silently when target is not defined', () => { - expect(() => { - mlChartTooltipService.show(getChartTooltipDefaultState().tooltipData, null); - }).not.toThrow('Call to show() should fail silently.'); + test('update the tooltip state only on a new value', () => { + const spy = jest.fn(); + + service.tooltipState$.subscribe(spy); + + expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState()); + + service.hide(); + + expect(spy).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts new file mode 100644 index 0000000000000..b524e18102a95 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts @@ -0,0 +1,73 @@ +/* + * 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 { BehaviorSubject, Observable } from 'rxjs'; +import { isEqual } from 'lodash'; +import { TooltipValue, TooltipValueFormatter } from '@elastic/charts'; +import { distinctUntilChanged } from 'rxjs/operators'; + +export interface ChartTooltipValue extends TooltipValue { + skipHeader?: boolean; +} + +export interface TooltipHeader { + skipHeader: boolean; +} + +export type TooltipData = ChartTooltipValue[]; + +export interface ChartTooltipState { + isTooltipVisible: boolean; + offset: TooltipOffset; + tooltipData: TooltipData; + tooltipHeaderFormatter?: TooltipValueFormatter; + target: HTMLElement | null; +} + +interface TooltipOffset { + x: number; + y: number; +} + +export const getChartTooltipDefaultState = (): ChartTooltipState => ({ + isTooltipVisible: false, + tooltipData: ([] as unknown) as TooltipData, + offset: { x: 0, y: 0 }, + target: null, +}); + +export class ChartTooltipService { + private chartTooltip$ = new BehaviorSubject(getChartTooltipDefaultState()); + + public tooltipState$: Observable = this.chartTooltip$ + .asObservable() + .pipe(distinctUntilChanged(isEqual)); + + public show( + tooltipData: TooltipData, + target: HTMLElement, + offset: TooltipOffset = { x: 0, y: 0 } + ) { + if (!target) { + throw new Error('target is required for the tooltip positioning'); + } + + this.chartTooltip$.next({ + ...this.chartTooltip$.getValue(), + isTooltipVisible: true, + offset, + tooltipData, + target, + }); + } + + public hide() { + this.chartTooltip$.next({ + ...getChartTooltipDefaultState(), + isTooltipVisible: false, + }); + } +} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts index 75c65ebaa0f50..ec19fe18bd324 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { mlChartTooltipService } from './chart_tooltip_service'; -export { ChartTooltip } from './chart_tooltip'; +export { ChartTooltipService } from './chart_tooltip_service'; +export { MlTooltipComponent } from './chart_tooltip'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 381e5e75356c1..f709c161bef17 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -4,45 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiSwitch, - EuiTitle, -} from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../contexts/kibana'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../services/ml_api_service'; import { useUrlState } from '../../util/url_state'; // @ts-ignore -import { JobSelectorTable } from './job_selector_table/index'; -// @ts-ignore import { IdBadges } from './id_badges/index'; -// @ts-ignore -import { NewSelectionIdBadges } from './new_selection_id_badges/index'; -import { - getGroupsFromJobs, - getTimeRangeFromSelection, - normalizeTimes, -} from './job_select_service_utils'; +import { BADGE_LIMIT, JobSelectorFlyout, JobSelectorFlyoutProps } from './job_selector_flyout'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; interface GroupObj { groupId: string; jobIds: string[]; } + function mergeSelection( jobIds: string[], groupObjs: GroupObj[], @@ -71,7 +49,7 @@ function mergeSelection( } type GroupsMap = Dictionary; -function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { +export function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { const map: GroupsMap = {}; if (selectedGroups.length) { @@ -83,81 +61,38 @@ function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { return map; } -const BADGE_LIMIT = 10; -const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels - interface JobSelectorProps { dateFormatTz: string; singleSelection: boolean; timeseriesOnly: boolean; } +export interface JobSelectionMaps { + jobsMap: Dictionary; + groupsMap: Dictionary; +} + export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: JobSelectorProps) { const [globalState, setGlobalState] = useUrlState('_g'); const selectedJobIds = globalState?.ml?.jobIds ?? []; const selectedGroups = globalState?.ml?.groups ?? []; - const [jobs, setJobs] = useState([]); - const [groups, setGroups] = useState([]); - const [maps, setMaps] = useState({ groupsMap: getInitialGroupsMap(selectedGroups), jobsMap: {} }); + const [maps, setMaps] = useState({ + groupsMap: getInitialGroupsMap(selectedGroups), + jobsMap: {}, + }); const [selectedIds, setSelectedIds] = useState( mergeSelection(selectedJobIds, selectedGroups, singleSelection) ); - const [newSelection, setNewSelection] = useState( - mergeSelection(selectedJobIds, selectedGroups, singleSelection) - ); - const [showAllBadges, setShowAllBadges] = useState(false); const [showAllBarBadges, setShowAllBarBadges] = useState(false); - const [applyTimeRange, setApplyTimeRange] = useState(true); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); - const flyoutEl = useRef<{ flyout: HTMLElement }>(null); - const { - services: { notifications }, - } = useMlKibana(); // Ensure JobSelectionBar gets updated when selection via globalState changes. useEffect(() => { setSelectedIds(mergeSelection(selectedJobIds, selectedGroups, singleSelection)); }, [JSON.stringify([selectedJobIds, selectedGroups])]); - // Ensure current selected ids always show up in flyout - useEffect(() => { - setNewSelection(selectedIds); - }, [isFlyoutVisible]); // eslint-disable-line - - // Wrap handleResize in useCallback as it is a dependency for useEffect on line 131 below. - // Not wrapping it would cause this dependency to change on every render - const handleResize = useCallback(() => { - if (jobs.length > 0 && flyoutEl && flyoutEl.current && flyoutEl.current.flyout) { - // get all cols in flyout table - const tableHeaderCols: NodeListOf = flyoutEl.current.flyout.querySelectorAll( - 'table thead th' - ); - // get the width of the last col - const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16; - const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth); - setJobs(normalizedJobs); - const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs); - setGroups(updatedGroups); - setGanttBarWidth(derivedWidth); - } - }, [dateFormatTz, jobs]); - - useEffect(() => { - // Ensure ganttBar width gets calculated on resize - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [handleResize]); - - useEffect(() => { - handleResize(); - }, [handleResize, jobs]); - function closeFlyout() { setIsFlyoutVisible(false); } @@ -168,78 +103,26 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function handleJobSelectionClick() { showFlyout(); - - ml.jobs - .jobsWithTimerange(dateFormatTz) - .then(resp => { - const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH); - const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); - setJobs(normalizedJobs); - setGroups(groupsWithTimerange); - setMaps({ groupsMap, jobsMap: resp.jobsMap }); - }) - .catch((err: any) => { - console.error('Error fetching jobs with time range', err); // eslint-disable-line - const { toasts } = notifications; - toasts.addDanger({ - title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { - defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', - }), - }); - }); - } - - function handleNewSelection({ selectionFromTable }: { selectionFromTable: any }) { - setNewSelection(selectionFromTable); } - function applySelection() { - // allNewSelection will be a list of all job ids (including those from groups) selected from the table - const allNewSelection: string[] = []; - const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; - - newSelection.forEach(id => { - if (maps.groupsMap[id] !== undefined) { - // Push all jobs from selected groups into the newSelection list - allNewSelection.push(...maps.groupsMap[id]); - // if it's a group - push group obj to set in global state - groupSelection.push({ groupId: id, jobIds: maps.groupsMap[id] }); - } else { - allNewSelection.push(id); - } - }); - // create a Set to remove duplicate values - const allNewSelectionUnique = Array.from(new Set(allNewSelection)); - + const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({ + newSelection, + jobIds, + groups: newGroups, + time, + }) => { setSelectedIds(newSelection); - setNewSelection([]); - - closeFlyout(); - - const time = applyTimeRange - ? getTimeRangeFromSelection(jobs, allNewSelectionUnique) - : undefined; setGlobalState({ ml: { - jobIds: allNewSelectionUnique, - groups: groupSelection, + jobIds, + groups: newGroups, }, ...(time !== undefined ? { time } : {}), }); - } - - function toggleTimerangeSwitch() { - setApplyTimeRange(!applyTimeRange); - } - - function removeId(id: string) { - setNewSelection(newSelection.filter(item => item !== id)); - } - function clearSelection() { - setNewSelection([]); - } + closeFlyout(); + }; function renderJobSelectionBar() { return ( @@ -280,103 +163,16 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function renderFlyout() { if (isFlyoutVisible) { return ( - - - -

- {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { - defaultMessage: 'Job selection', - })} -

-
-
- - - - - setShowAllBadges(!showAllBadges)} - showAllBadges={showAllBadges} - /> - - - - - - {!singleSelection && newSelection.length > 0 && ( - - {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { - defaultMessage: 'Clear all', - })} - - )} - - - - - - - - - - - - - - {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { - defaultMessage: 'Apply', - })} - - - - - {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { - defaultMessage: 'Close', - })} - - - - -
+ ); } } @@ -388,9 +184,3 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J
); } - -JobSelector.propTypes = { - selectedJobIds: PropTypes.array, - singleSelection: PropTypes.bool, - timeseriesOnly: PropTypes.bool, -}; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js rename to x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.ts diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx similarity index 68% rename from x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js rename to x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx index 4d2ab01e2a054..b2cae278c0e77 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx @@ -4,18 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { PropTypes } from 'prop-types'; -import { EuiBadge } from '@elastic/eui'; -import { tabColor } from '../../../../../common/util/group_color_utils'; +import React, { FC } from 'react'; +import { EuiBadge, EuiBadgeProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { tabColor } from '../../../../../common/util/group_color_utils'; -export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId }) { +interface JobSelectorBadgeProps { + icon?: boolean; + id: string; + isGroup?: boolean; + numJobs?: number; + removeId?: Function; +} + +export const JobSelectorBadge: FC = ({ + icon, + id, + isGroup = false, + numJobs, + removeId, +}) => { const color = isGroup ? tabColor(id) : 'hollow'; - let props = { color }; + let props = { color } as EuiBadgeProps; let jobCount; - if (icon === true) { + if (icon === true && removeId) { + // @ts-ignore props = { ...props, iconType: 'cross', @@ -37,11 +51,4 @@ export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId {`${id}${jobCount ? jobCount : ''}`} ); -} -JobSelectorBadge.propTypes = { - icon: PropTypes.bool, - id: PropTypes.string.isRequired, - isGroup: PropTypes.bool, - numJobs: PropTypes.number, - removeId: PropTypes.func, }; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx new file mode 100644 index 0000000000000..66aa05d2aaa97 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -0,0 +1,289 @@ +/* + * 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, useCallback, useEffect, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSwitch, + EuiTitle, +} from '@elastic/eui'; +import { NewSelectionIdBadges } from './new_selection_id_badges'; +// @ts-ignore +import { JobSelectorTable } from './job_selector_table'; +import { + getGroupsFromJobs, + getTimeRangeFromSelection, + normalizeTimes, +} from './job_select_service_utils'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../services/ml_api_service'; +import { useMlKibana } from '../../contexts/kibana'; +import { JobSelectionMaps } from './job_selector'; + +export const BADGE_LIMIT = 10; +export const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels + +export interface JobSelectorFlyoutProps { + dateFormatTz: string; + selectedIds?: string[]; + newSelection?: string[]; + onFlyoutClose: () => void; + onJobsFetched?: (maps: JobSelectionMaps) => void; + onSelectionChange?: (newSelection: string[]) => void; + onSelectionConfirmed: (payload: { + newSelection: string[]; + jobIds: string[]; + groups: Array<{ groupId: string; jobIds: string[] }>; + time: any; + }) => void; + singleSelection: boolean; + timeseriesOnly: boolean; + maps: JobSelectionMaps; + withTimeRangeSelector?: boolean; +} + +export const JobSelectorFlyout: FC = ({ + dateFormatTz, + selectedIds = [], + singleSelection, + timeseriesOnly, + onJobsFetched, + onSelectionChange, + onSelectionConfirmed, + onFlyoutClose, + maps, + withTimeRangeSelector = true, +}) => { + const { + services: { notifications }, + } = useMlKibana(); + + const [newSelection, setNewSelection] = useState(selectedIds); + + const [showAllBadges, setShowAllBadges] = useState(false); + const [applyTimeRange, setApplyTimeRange] = useState(true); + const [jobs, setJobs] = useState([]); + const [groups, setGroups] = useState([]); + const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); + const [jobGroupsMaps, setJobGroupsMaps] = useState(maps); + + const flyoutEl = useRef<{ flyout: HTMLElement }>(null); + + function applySelection() { + // allNewSelection will be a list of all job ids (including those from groups) selected from the table + const allNewSelection: string[] = []; + const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; + + newSelection.forEach(id => { + if (jobGroupsMaps.groupsMap[id] !== undefined) { + // Push all jobs from selected groups into the newSelection list + allNewSelection.push(...jobGroupsMaps.groupsMap[id]); + // if it's a group - push group obj to set in global state + groupSelection.push({ groupId: id, jobIds: jobGroupsMaps.groupsMap[id] }); + } else { + allNewSelection.push(id); + } + }); + // create a Set to remove duplicate values + const allNewSelectionUnique = Array.from(new Set(allNewSelection)); + + const time = applyTimeRange + ? getTimeRangeFromSelection(jobs, allNewSelectionUnique) + : undefined; + + onSelectionConfirmed({ + newSelection: allNewSelectionUnique, + jobIds: allNewSelectionUnique, + groups: groupSelection, + time, + }); + } + + function removeId(id: string) { + setNewSelection(newSelection.filter(item => item !== id)); + } + + function toggleTimerangeSwitch() { + setApplyTimeRange(!applyTimeRange); + } + + function clearSelection() { + setNewSelection([]); + } + + function handleNewSelection({ selectionFromTable }: { selectionFromTable: any }) { + setNewSelection(selectionFromTable); + } + + // Wrap handleResize in useCallback as it is a dependency for useEffect on line 131 below. + // Not wrapping it would cause this dependency to change on every render + const handleResize = useCallback(() => { + if (jobs.length > 0 && flyoutEl && flyoutEl.current && flyoutEl.current.flyout) { + // get all cols in flyout table + const tableHeaderCols: NodeListOf = flyoutEl.current.flyout.querySelectorAll( + 'table thead th' + ); + // get the width of the last col + const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16; + const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth); + setJobs(normalizedJobs); + const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs); + setGroups(updatedGroups); + setGanttBarWidth(derivedWidth); + } + }, [dateFormatTz, jobs]); + + // Fetch jobs list on flyout open + useEffect(() => { + fetchJobs(); + }, []); + + async function fetchJobs() { + try { + const resp = await ml.jobs.jobsWithTimerange(dateFormatTz); + const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH); + const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); + setJobs(normalizedJobs); + setGroups(groupsWithTimerange); + setJobGroupsMaps({ groupsMap, jobsMap: resp.jobsMap }); + + if (onJobsFetched) { + onJobsFetched({ groupsMap, jobsMap: resp.jobsMap }); + } + } catch (e) { + console.error('Error fetching jobs with time range', e); // eslint-disable-line + const { toasts } = notifications; + toasts.addDanger({ + title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { + defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', + }), + }); + } + } + + useEffect(() => { + // Ensure ganttBar width gets calculated on resize + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [handleResize]); + + useEffect(() => { + handleResize(); + }, [handleResize, jobs]); + + return ( + + + +

+ {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { + defaultMessage: 'Job selection', + })} +

+
+
+ + + + + setShowAllBadges(!showAllBadges)} + showAllBadges={showAllBadges} + /> + + + + + + {!singleSelection && newSelection.length > 0 && ( + + {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { + defaultMessage: 'Clear all', + })} + + )} + + {withTimeRangeSelector && ( + + + + )} + + + + + + + + + + {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { + defaultMessage: 'Apply', + })} + + + + + {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { + defaultMessage: 'Close', + })} + + + + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index 64793d15f1e4a..c55e03776c09d 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -224,7 +224,7 @@ export function JobSelectorTable({ {jobs.length === 0 && } {jobs.length !== 0 && singleSelection === true && renderJobsTable()} - {jobs.length !== 0 && singleSelection === undefined && renderTabs()} + {jobs.length !== 0 && !singleSelection && renderTabs()} ); } diff --git a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js rename to x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.ts diff --git a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx similarity index 80% rename from x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js rename to x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx index 67dce47323889..4c018e72f3e10 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx @@ -4,20 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { PropTypes } from 'prop-types'; +import React, { FC, MouseEventHandler } from 'react'; import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; -import { JobSelectorBadge } from '../job_selector_badge'; import { i18n } from '@kbn/i18n'; +import { JobSelectorBadge } from '../job_selector_badge'; +import { JobSelectionMaps } from '../job_selector'; -export function NewSelectionIdBadges({ +interface NewSelectionIdBadgesProps { + limit: number; + maps: JobSelectionMaps; + newSelection: string[]; + onDeleteClick?: Function; + onLinkClick?: MouseEventHandler; + showAllBadges?: boolean; +} + +export const NewSelectionIdBadges: FC = ({ limit, maps, newSelection, onDeleteClick, onLinkClick, showAllBadges, -}) { +}) => { const badges = []; for (let i = 0; i < newSelection.length; i++) { @@ -60,16 +69,5 @@ export function NewSelectionIdBadges({ ); } - return badges; -} -NewSelectionIdBadges.propTypes = { - limit: PropTypes.number, - maps: PropTypes.shape({ - jobsMap: PropTypes.object, - groupsMap: PropTypes.object, - }), - newSelection: PropTypes.array, - onDeleteClick: PropTypes.func, - onLinkClick: PropTypes.func, - showAllBadges: PropTypes.bool, + return <>{badges}; }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 86ffc4a2614b9..06d89ab782167 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -41,7 +41,7 @@ import { useMlContext } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; import { useTimefilter } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { useUrlState } from '../../util/url_state'; import { FieldRequestConfig, FieldVisConfig } from './common'; import { ActionsPanel } from './components/actions_panel'; @@ -318,7 +318,7 @@ export const Page: FC = () => { // Obtain the interval to use for date histogram aggregations // (such as the document count chart). Aim for 75 bars. - const buckets = new TimeBuckets(); + const buckets = getTimeBucketsFromCache(); const tf = timefilter as any; let earliest: number | undefined; diff --git a/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap similarity index 100% rename from x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap rename to x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 9fb2f0c3bed94..cfcba081983c2 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -106,164 +106,6 @@ padding: 0; margin-bottom: $euiSizeS; - div.ml-swimlanes { - margin: 0px 0px 0px 10px; - - div.cells-marker-container { - margin-left: 176px; - height: 22px; - white-space: nowrap; - - // background-color: #CCC; - .sl-cell { - height: 10px; - display: inline-block; - vertical-align: top; - margin-top: 16px; - text-align: center; - visibility: hidden; - cursor: default; - - i { - color: $euiColorDarkShade; - } - } - - .sl-cell-hover { - visibility: visible; - - i { - display: block; - margin-top: -6px; - } - } - - .sl-cell-active-hover { - visibility: visible; - - .floating-time-label { - display: inline-block; - } - } - } - - div.lane { - height: 30px; - border-bottom: 0px; - border-radius: 2px; - margin-top: -1px; - white-space: nowrap; - - div.lane-label { - display: inline-block; - font-size: 13px; - height: 30px; - text-align: right; - vertical-align: middle; - border-radius: 2px; - padding-right: 5px; - margin-right: 5px; - border: 1px solid transparent; - overflow: hidden; - text-overflow: ellipsis; - } - - div.lane-label.lane-label-masked { - opacity: 0.3; - } - - div.cells-container { - border: $euiBorderThin; - border-right: 0px; - display: inline-block; - height: 30px; - vertical-align: middle; - background-color: $euiColorEmptyShade; - - .sl-cell { - color: $euiColorEmptyShade; - cursor: default; - display: inline-block; - height: 29px; - border-right: $euiBorderThin; - vertical-align: top; - position: relative; - - .sl-cell-inner, - .sl-cell-inner-dragselect { - height: 26px; - margin: 1px; - border-radius: 2px; - text-align: center; - } - - .sl-cell-inner.sl-cell-inner-masked { - opacity: 0.2; - } - - .sl-cell-inner.sl-cell-inner-selected, - .sl-cell-inner-dragselect.sl-cell-inner-selected { - border: 2px solid $euiColorDarkShade; - } - - .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, - .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { - border: 2px solid $euiColorFullShade; - opacity: 0.4; - } - } - - .sl-cell:hover { - .sl-cell-inner { - opacity: 0.8; - cursor: pointer; - } - } - - .sl-cell.ds-selected { - - .sl-cell-inner, - .sl-cell-inner-dragselect { - border: 2px solid $euiColorDarkShade; - border-radius: 2px; - opacity: 1; - } - } - - } - } - - div.lane:last-child { - div.cells-container { - .sl-cell { - border-bottom: $euiBorderThin; - } - } - } - - .time-tick-labels { - height: 25px; - margin-top: $euiSizeXS / 2; - margin-left: 175px; - - /* hide d3's domain line */ - path.domain { - display: none; - } - - /* hide d3's tick line */ - g.tick line { - display: none; - } - - /* override d3's default tick styles */ - g.tick text { - font-size: 11px; - fill: $euiColorMediumShade; - } - } - } - line.gridLine { stroke: $euiBorderColor; fill: none; @@ -328,3 +170,161 @@ } } } + +.ml-swimlanes { + margin: 0px 0px 0px 10px; + + div.cells-marker-container { + margin-left: 176px; + height: 22px; + white-space: nowrap; + + // background-color: #CCC; + .sl-cell { + height: 10px; + display: inline-block; + vertical-align: top; + margin-top: 16px; + text-align: center; + visibility: hidden; + cursor: default; + + i { + color: $euiColorDarkShade; + } + } + + .sl-cell-hover { + visibility: visible; + + i { + display: block; + margin-top: -6px; + } + } + + .sl-cell-active-hover { + visibility: visible; + + .floating-time-label { + display: inline-block; + } + } + } + + div.lane { + height: 30px; + border-bottom: 0px; + border-radius: 2px; + margin-top: -1px; + white-space: nowrap; + + div.lane-label { + display: inline-block; + font-size: 13px; + height: 30px; + text-align: right; + vertical-align: middle; + border-radius: 2px; + padding-right: 5px; + margin-right: 5px; + border: 1px solid transparent; + overflow: hidden; + text-overflow: ellipsis; + } + + div.lane-label.lane-label-masked { + opacity: 0.3; + } + + div.cells-container { + border: $euiBorderThin; + border-right: 0px; + display: inline-block; + height: 30px; + vertical-align: middle; + background-color: $euiColorEmptyShade; + + .sl-cell { + color: $euiColorEmptyShade; + cursor: default; + display: inline-block; + height: 29px; + border-right: $euiBorderThin; + vertical-align: top; + position: relative; + + .sl-cell-inner, + .sl-cell-inner-dragselect { + height: 26px; + margin: 1px; + border-radius: 2px; + text-align: center; + } + + .sl-cell-inner.sl-cell-inner-masked { + opacity: 0.2; + } + + .sl-cell-inner.sl-cell-inner-selected, + .sl-cell-inner-dragselect.sl-cell-inner-selected { + border: 2px solid $euiColorDarkShade; + } + + .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, + .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { + border: 2px solid $euiColorFullShade; + opacity: 0.4; + } + } + + .sl-cell:hover { + .sl-cell-inner { + opacity: 0.8; + cursor: pointer; + } + } + + .sl-cell.ds-selected { + + .sl-cell-inner, + .sl-cell-inner-dragselect { + border: 2px solid $euiColorDarkShade; + border-radius: 2px; + opacity: 1; + } + } + + } + } + + div.lane:last-child { + div.cells-container { + .sl-cell { + border-bottom: $euiBorderThin; + } + } + } + + .time-tick-labels { + height: 25px; + margin-top: $euiSizeXS / 2; + margin-left: 175px; + + /* hide d3's domain line */ + path.domain { + display: none; + } + + /* hide d3's tick line */ + g.tick line { + display: none; + } + + /* override d3's default tick styles */ + g.tick text { + font-size: 11px; + fill: $euiColorMediumShade; + } + } +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index d61d56d07b644..86d16776b68e2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -36,9 +36,8 @@ import { ExplorerNoJobsFound, ExplorerNoResultsFound, } from './components'; -import { ChartTooltip } from '../components/chart_tooltip'; import { ExplorerSwimlane } from './explorer_swimlane'; -import { TimeBuckets } from '../util/time_buckets'; +import { getTimeBucketsFromCache } from '../util/time_buckets'; import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, @@ -81,6 +80,7 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { getTimefilter, getToastNotifications } from '../util/dependency_cache'; +import { MlTooltipComponent } from '../components/chart_tooltip'; function mapSwimlaneOptionsToEuiOptions(options) { return options.map(option => ({ @@ -179,6 +179,8 @@ export class Explorer extends React.Component { // Required to redraw the time series chart when the container is resized. this.resizeChecker = new ResizeChecker(this.resizeRef.current); this.resizeChecker.on('resize', this.resizeHandler); + + this.timeBuckets = getTimeBucketsFromCache(); } componentWillUnmount() { @@ -358,9 +360,6 @@ export class Explorer extends React.Component { return (
- {/* Make sure ChartTooltip is inside wrapping div with 0px left/right padding so positioning can be inferred correctly. */} - - {noInfluencersConfigured === false && influencers !== undefined && (
{showOverallSwimlane && ( - + + {tooltipService => ( + + )} + )}
@@ -494,17 +498,22 @@ export class Explorer extends React.Component { onMouseLeave={this.onSwimlaneLeaveHandler} data-test-subj="mlAnomalyExplorerSwimlaneViewBy" > - + + {tooltipService => ( + + )} +
)} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 5fc1160093a49..03426869b0ccf 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -29,9 +29,8 @@ import { removeLabelOverlap, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; import { CHART_TYPE } from '../explorer_constants'; @@ -50,6 +49,7 @@ export class ExplorerChartDistribution extends React.Component { static propTypes = { seriesConfig: PropTypes.object, severity: PropTypes.number, + tooltipService: PropTypes.object.isRequired, }; componentDidMount() { @@ -61,7 +61,7 @@ export class ExplorerChartDistribution extends React.Component { } renderChart() { - const { tooManyBuckets } = this.props; + const { tooManyBuckets, tooltipService } = this.props; const element = this.rootNode; const config = this.props.seriesConfig; @@ -259,7 +259,7 @@ export class ExplorerChartDistribution extends React.Component { function drawRareChartAxes() { // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); + const timeBuckets = getTimeBucketsFromCache(); const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; timeBuckets.setBounds(bounds); timeBuckets.setInterval('auto'); @@ -397,7 +397,7 @@ export class ExplorerChartDistribution extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); // Update all dots to new positions. dots @@ -550,7 +550,7 @@ export class ExplorerChartDistribution extends React.Component { }); } - mlChartTooltipService.show(tooltipData, circle, { + tooltipService.show(tooltipData, circle, { x: LINE_CHART_ANOMALY_RADIUS * 3, y: LINE_CHART_ANOMALY_RADIUS * 2, }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 71d777db5b2ec..06fd82204c1e1 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -10,11 +10,13 @@ import seriesConfig from './__mocks__/mock_series_config_rare.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. jest.mock('../../util/time_buckets', () => ({ - TimeBuckets: function() { - this.setBounds = jest.fn(); - this.setInterval = jest.fn(); - this.getScaledDateFormat = jest.fn(); - }, + getTimeBucketsFromCache: jest.fn(() => { + return { + setBounds: jest.fn(), + setInterval: jest.fn(), + getScaledDateFormat: jest.fn(), + }; + }), })); jest.mock('../../services/field_format_service', () => ({ mlFieldFormatService: { @@ -43,8 +45,16 @@ describe('ExplorerChart', () => { afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Initialize', () => { + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -59,10 +69,16 @@ describe('ExplorerChart', () => { loading: true, }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( ); @@ -83,12 +99,18 @@ describe('ExplorerChart', () => { chartLimits: chartLimits(chartData), }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + // We create the element including a wrapper which sets the width: return mountWithIntl(
); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index dd9479be931a7..82041af39ca15 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -38,10 +38,9 @@ import { showMultiBucketAnomalyTooltip, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; import { i18n } from '@kbn/i18n'; @@ -53,6 +52,7 @@ export class ExplorerChartSingleMetric extends React.Component { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, severity: PropTypes.number.isRequired, + tooltipService: PropTypes.object.isRequired, }; componentDidMount() { @@ -64,7 +64,7 @@ export class ExplorerChartSingleMetric extends React.Component { } renderChart() { - const { tooManyBuckets } = this.props; + const { tooManyBuckets, tooltipService } = this.props; const element = this.rootNode; const config = this.props.seriesConfig; @@ -191,7 +191,7 @@ export class ExplorerChartSingleMetric extends React.Component { function drawLineChartAxes() { // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); + const timeBuckets = getTimeBucketsFromCache(); const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; timeBuckets.setBounds(bounds); timeBuckets.setInterval('auto'); @@ -309,7 +309,7 @@ export class ExplorerChartSingleMetric extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); const isAnomalyVisible = d => _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; @@ -354,7 +354,7 @@ export class ExplorerChartSingleMetric extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); // Add rectangular markers for any scheduled events. const scheduledEventMarkers = lineChartGroup @@ -503,7 +503,7 @@ export class ExplorerChartSingleMetric extends React.Component { }); } - mlChartTooltipService.show(tooltipData, circle, { + tooltipService.show(tooltipData, circle, { x: LINE_CHART_ANOMALY_RADIUS * 3, y: LINE_CHART_ANOMALY_RADIUS * 2, }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index ca3e52308a936..54f541ceb7c3d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -10,11 +10,13 @@ import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. jest.mock('../../util/time_buckets', () => ({ - TimeBuckets: function() { - this.setBounds = jest.fn(); - this.setInterval = jest.fn(); - this.getScaledDateFormat = jest.fn(); - }, + getTimeBucketsFromCache: jest.fn(() => { + return { + setBounds: jest.fn(), + setInterval: jest.fn(), + getScaledDateFormat: jest.fn(), + }; + }), })); jest.mock('../../services/field_format_service', () => ({ mlFieldFormatService: { @@ -43,8 +45,16 @@ describe('ExplorerChart', () => { afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Initialize', () => { + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -59,10 +69,16 @@ describe('ExplorerChart', () => { loading: true, }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( ); @@ -83,12 +99,18 @@ describe('ExplorerChart', () => { chartLimits: chartLimits(chartData), }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + // We create the element including a wrapper which sets the width: return mountWithIntl(
); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 99de38c1e0a84..5b95931d31ab6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import $ from 'jquery'; - import React from 'react'; import { @@ -29,6 +27,7 @@ import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MlTooltipComponent } from '../../components/chart_tooltip'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { defaultMessage: @@ -121,19 +120,29 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) chartType === CHART_TYPE.POPULATION_DISTRIBUTION ) { return ( - + + {tooltipService => ( + + )} + ); } return ( - + + {tooltipService => ( + + )} + ); })()} @@ -141,48 +150,36 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) } // Flex layout wrapper for all explorer charts -export class ExplorerChartsContainer extends React.Component { - componentDidMount() { - // Create a div for the tooltip. - $('.ml-explorer-charts-tooltip').remove(); - $('body').append( - '