From 970de9147ea2b51488e63dd71e28b9df7a7bf138 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 18 Apr 2023 08:44:16 +0200 Subject: [PATCH 001/100] [Fleet] Surface new overview dashboards in fleet (#154914) Closes https://github.com/elastic/kibana/issues/153848 ## Summary - Adds two link buttons on top of agent list page to access "Ingest overview" and "Agent Info" dashboards Screenshot 2023-04-13 at 15 22 53 The links are built using the new URL service [locator](https://github.com/elastic/kibana/blob/e80abe810837eeeff7fdcd594c6f8950590b49cd/x-pack/plugins/fleet/public/hooks/use_locator.ts#L14) and the [getRedirectLink](https://github.com/elastic/kibana/blob/main/src/plugins/share/README.mdx#using-locator-of-another-app) method; - Refactoring existing instances of `useKibanaLink` to use the url locator instead; These new dashboards were already accessible from the ` elastic_agent.*` datastreams table actions, however I replaced the `useKibanaLink` hook there as well: Screenshot 2023-04-13 at 16 03 47 TODO: I don't know where to add the "Integrations" dashboard yet, I'm not sure it should go on the Integrations details page. ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/common/constants/epm.ts | 3 -- .../plugins/fleet/common/constants/index.ts | 1 + .../fleet/common/constants/locators.ts | 19 +++++++ x-pack/plugins/fleet/common/index.ts | 2 + .../components/agent_dashboard_link.test.tsx | 11 ++++ .../components/agent_dashboard_link.tsx | 22 +++++--- .../components/dashboards_buttons.tsx | 53 +++++++++++++++++++ .../components/search_and_filter_bar.test.tsx | 11 ++++ .../components/search_and_filter_bar.tsx | 21 ++++---- .../components/data_stream_row_actions.tsx | 10 ++-- .../plugins/fleet/public/constants/index.ts | 5 +- x-pack/plugins/fleet/public/hooks/index.ts | 1 + .../plugins/fleet/public/hooks/use_locator.ts | 6 ++- .../apis/integrations/elastic_agent.ts | 11 ++-- 14 files changed, 140 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/fleet/common/constants/locators.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/dashboards_buttons.tsx diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index f380d66d0223c..a1d73b452cf72 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -22,9 +22,6 @@ export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; export const PACKAGE_TEMPLATE_SUFFIX = '@package'; export const USER_SETTINGS_TEMPLATE_SUFFIX = '@custom'; -export const FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID = - 'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395'; - export const DATASET_VAR_NAME = 'data_stream.dataset'; /* Package rules: diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 4dcc2d58d65ba..9892c883805f7 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -20,6 +20,7 @@ export * from './fleet_server_policy_config'; export * from './authz'; export * from './file_storage'; export * from './message_signing_keys'; +export * from './locators'; // TODO: This is the default `index.max_result_window` ES setting, which dictates // the maximum amount of results allowed to be returned from a search. It's possible diff --git a/x-pack/plugins/fleet/common/constants/locators.ts b/x-pack/plugins/fleet/common/constants/locators.ts new file mode 100644 index 0000000000000..daa00bcae46e0 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/locators.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LOCATORS_IDS = { + APM_LOCATOR: 'APM_LOCATOR', + DASHBOARD_APP: 'DASHBOARD_APP_LOCATOR', +} as const; + +// Dashboards ids +export const DASHBOARD_LOCATORS_IDS = { + ELASTIC_AGENT_OVERVIEW: 'elastic_agent-a148dc70-6b3c-11ed-98de-67bdecd21824', + ELASTIC_AGENT_AGENT_INFO: 'elastic_agent-0600ffa0-6b5e-11ed-98de-67bdecd21824', + ELASTIC_AGENT_AGENT_METRICS: 'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395', + ELASTIC_AGENT_INTEGRATIONS: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824', +} as const; diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 3cdfa354a8c5f..3ca552966a7c5 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -52,6 +52,8 @@ export { // Statuses // Authz ENDPOINT_PRIVILEGES, + // dashboards ids + DASHBOARD_LOCATORS_IDS, } from './constants'; export { // Route services diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx index 7a65be0a460c9..79908819ff863 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx @@ -25,6 +25,17 @@ jest.mock('../../../../../../hooks/use_fleet_status', () => ({ jest.mock('../../../../../../hooks/use_request/epm'); +jest.mock('../../../../../../hooks/use_locator', () => { + return { + useDashboardLocator: jest.fn().mockImplementation(() => { + return { + id: 'DASHBOARD_APP_LOCATOR', + getRedirectUrl: jest.fn().mockResolvedValue('app/dashboards#/view/elastic_agent-a0001'), + }; + }), + }; +}); + describe('AgentDashboardLink', () => { it('should enable the button if elastic_agent package is installed and policy has monitoring enabled', async () => { mockedUseGetPackageInfoByKeyQuery.mockReturnValue({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx index 6c7479fd8c801..6832f81961ddb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx @@ -10,21 +10,26 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; -import { useGetPackageInfoByKeyQuery, useKibanaLink, useLink } from '../../../../hooks'; +import { useGetPackageInfoByKeyQuery, useLink, useDashboardLocator } from '../../../../hooks'; import type { Agent, AgentPolicy } from '../../../../types'; import { FLEET_ELASTIC_AGENT_PACKAGE, - FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID, + DASHBOARD_LOCATORS_IDS, } from '../../../../../../../common/constants'; function useAgentDashboardLink(agent: Agent) { const { isLoading, data } = useGetPackageInfoByKeyQuery(FLEET_ELASTIC_AGENT_PACKAGE); const isInstalled = data?.item.status === 'installed'; + const dashboardLocator = useDashboardLocator(); - const dashboardLink = useKibanaLink(`/dashboard/${FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID}`); - const query = `_a=(query:(language:kuery,query:'elastic_agent.id:${agent.id}'))`; - const link = `${dashboardLink}?${query}`; + const link = dashboardLocator?.getRedirectUrl({ + dashboardId: DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS, + query: { + language: 'kuery', + query: `elastic_agent.id:${agent.id}`, + }, + }); return { isLoading, @@ -50,7 +55,12 @@ export const AgentDashboardLink: React.FunctionComponent<{ !isInstalled || isLoading || !isLogAndMetricsEnabled ? { disabled: true } : { href: link }; const button = ( - + { + const dashboardLocator = useDashboardLocator(); + + const getDashboardHref = (dashboardId: string) => { + return dashboardLocator?.getRedirectUrl({ dashboardId }) || ''; + }; + + return ( + <> + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx index e239f31ed3adc..9af04b04761a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx @@ -36,6 +36,17 @@ jest.mock('../../../../components', () => { }; }); +jest.mock('../../../../../../hooks/use_locator', () => { + return { + useDashboardLocator: jest.fn().mockImplementation(() => { + return { + id: 'DASHBOARD_APP_LOCATOR', + getRedirectUrl: jest.fn().mockResolvedValue('app/dashboards#/view/elastic_agent-a0002'), + }; + }), + }; +}); + const TestComponent = (props: any) => ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 4ca43020e132a..c9c527ba36e0a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -31,15 +31,12 @@ import { AgentBulkActions } from './bulk_actions'; import type { SelectionMode } from './types'; import { AgentActivityButton } from './agent_activity_button'; import { AgentStatusFilter } from './agent_status_filter'; +import { DashboardsButtons } from './dashboards_buttons'; const ClearAllTagsFilterItem = styled(EuiFilterSelectItem)` padding: ${(props) => props.theme.eui.euiSizeS}; `; -const FlexEndEuiFlexItem = styled(EuiFlexItem)` - align-self: flex-end; -`; - export const SearchAndFilterBar: React.FunctionComponent<{ agentPolicies: AgentPolicy[]; draftKuery: string; @@ -118,17 +115,18 @@ export const SearchAndFilterBar: React.FunctionComponent<{ return ( <> - {/* Search and filter bar */} - - - + {/* Top Buttons and Links */} + + {totalAgents > 0 && } + + - + - + - + + {/* Search and filters */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/components/data_stream_row_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/components/data_stream_row_actions.tsx index 7d88327e73fd6..8822117f56d67 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/components/data_stream_row_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/components/data_stream_row_actions.tsx @@ -10,13 +10,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DataStream } from '../../../../types'; -import { useKibanaLink } from '../../../../hooks'; +import { useDashboardLocator } from '../../../../hooks'; import { ContextMenuActions } from '../../../../components'; import { useAPMServiceDetailHref } from '../../../../hooks/use_apm_service_href'; export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastream }) => { const { dashboards } = datastream; + const dashboardLocator = useDashboardLocator(); + const actionNameSingular = ( (({ datastre items: [ { icon: 'dashboardApp', - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - href: useKibanaLink(`/dashboard/${dashboards[0].id || ''}`), + href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboards[0]?.id } || ''), name: actionNameSingular, }, ], @@ -109,8 +110,7 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre items: dashboards.map((dashboard) => { return { icon: 'dashboardApp', - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - href: useKibanaLink(`/dashboard/${dashboard.id || ''}`), + href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''), name: dashboard.title, }; }), diff --git a/x-pack/plugins/fleet/public/constants/index.ts b/x-pack/plugins/fleet/public/constants/index.ts index 22ba08f3f9a1a..2c682af1fcbcc 100644 --- a/x-pack/plugins/fleet/public/constants/index.ts +++ b/x-pack/plugins/fleet/public/constants/index.ts @@ -22,6 +22,7 @@ export { AUTO_UPDATE_PACKAGES, KEEP_POLICIES_UP_TO_DATE_PACKAGES, AUTO_UPGRADE_POLICIES_PACKAGES, + LOCATORS_IDS, } from '../../common/constants'; export * from './page_paths'; @@ -37,7 +38,3 @@ export const DURATION_APM_SETTINGS_VARS = { TAIL_SAMPLING_INTERVAL: 'tail_sampling_interval', WRITE_TIMEOUT: 'write_timeout', }; - -export const LOCATORS_IDS = { - APM_LOCATOR: 'APM_LOCATOR', -} as const; diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 0f7f6b2f4d165..a9fb6ef7758c7 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -31,3 +31,4 @@ export * from './use_flyout_context'; export * from './use_is_guided_onboarding_active'; export * from './use_fleet_server_hosts_for_policy'; export * from './use_fleet_server_standalone'; +export * from './use_locator'; diff --git a/x-pack/plugins/fleet/public/hooks/use_locator.ts b/x-pack/plugins/fleet/public/hooks/use_locator.ts index 46ec3e4c75d13..a3fed97679456 100644 --- a/x-pack/plugins/fleet/public/hooks/use_locator.ts +++ b/x-pack/plugins/fleet/public/hooks/use_locator.ts @@ -7,7 +7,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import type { ValuesType } from 'utility-types'; -import type { LOCATORS_IDS } from '../constants'; +import { LOCATORS_IDS } from '../constants'; import { useStartServices } from './use_core'; @@ -17,3 +17,7 @@ export function useLocator( const services = useStartServices(); return services.share.url.locators.get(locatorId); } + +export function useDashboardLocator() { + return useLocator(LOCATORS_IDS.DASHBOARD_APP); +} diff --git a/x-pack/test/fleet_api_integration/apis/integrations/elastic_agent.ts b/x-pack/test/fleet_api_integration/apis/integrations/elastic_agent.ts index 7fad10904a4f4..7cd94543cad30 100644 --- a/x-pack/test/fleet_api_integration/apis/integrations/elastic_agent.ts +++ b/x-pack/test/fleet_api_integration/apis/integrations/elastic_agent.ts @@ -6,10 +6,9 @@ */ import expect from '@kbn/expect'; -import { - FLEET_ELASTIC_AGENT_PACKAGE, - FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID, -} from '@kbn/fleet-plugin/common/constants/epm'; +import { FLEET_ELASTIC_AGENT_PACKAGE } from '@kbn/fleet-plugin/common/constants/epm'; + +import { DASHBOARD_LOCATORS_IDS } from '@kbn/fleet-plugin/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { setupFleetAndAgents } from '../agents/services'; @@ -47,10 +46,10 @@ export default function (providerContext: FtrProviderContext) { it('Install elastic agent details dashboard with the correct id', async () => { const resDashboard = await kibanaServer.savedObjects.get({ type: 'dashboard', - id: FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID, + id: DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS, }); - expect(resDashboard.id).to.eql(FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID); + expect(resDashboard.id).to.eql(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS); }); after(async () => { From 682f12ea77e8845c79952ccef4c5da2b0dee3e33 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 18 Apr 2023 09:11:07 +0200 Subject: [PATCH 002/100] [Discover][Saved Query] Add schema for Saved Query SO (#154230) ## Summary A follow up for https://github.com/elastic/kibana/pull/153131 This PR adds schema for Saved Query SO type. --- .../group2/check_registered_types.test.ts | 2 +- .../data/server/saved_objects/query.ts | 16 +++------- .../server/saved_objects/schemas/query.ts | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 src/plugins/data/server/saved_objects/schemas/query.ts diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 541bfa046b0d3..09977e1c272ab 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -123,7 +123,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-pack": "edd84b2c59ef36214ece0676706da8f22175c660", "osquery-pack-asset": "18e08979d46ee7e5538f54c080aec4d8c58516ca", "osquery-saved-query": "f5e4e303f65c7607248ea8b2672f1ee30e4fb15e", - "query": "ec6000b775f06f81470df42d23f7a88cb31d64ba", + "query": "cfc049e1f0574fb4fdb2d653d7c10bdc970a2610", "rules-settings": "9854495c3b54b16a6625fb250c35e5504da72266", "sample-data-telemetry": "c38daf1a49ed24f2a4fb091e6e1e833fccf19935", "search": "ed3a9b1681b57d69560909d51933fdf17576ea68", diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts index c32a13e85888a..cf75c28743d4f 100644 --- a/src/plugins/data/server/saved_objects/query.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -8,6 +8,7 @@ import { SavedObjectsType } from '@kbn/core/server'; import { savedQueryMigrations } from './migrations/query'; +import { SCHEMA_QUERY_V8_8_0 } from './schemas/query'; export const querySavedObjectType: SavedObjectsType = { name: 'query', @@ -29,21 +30,14 @@ export const querySavedObjectType: SavedObjectsType = { }, }, mappings: { + dynamic: false, properties: { title: { type: 'text' }, description: { type: 'text' }, - query: { - dynamic: false, - properties: { - language: { type: 'keyword' }, - }, - }, - filters: { - dynamic: false, - properties: {}, - }, - timefilter: { dynamic: false, properties: {} }, }, }, migrations: savedQueryMigrations, + schemas: { + '8.8.0': SCHEMA_QUERY_V8_8_0, + }, }; diff --git a/src/plugins/data/server/saved_objects/schemas/query.ts b/src/plugins/data/server/saved_objects/schemas/query.ts new file mode 100644 index 0000000000000..c460a06b9727a --- /dev/null +++ b/src/plugins/data/server/saved_objects/schemas/query.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; + +// As per `SavedQueryAttributes` +export const SCHEMA_QUERY_V8_8_0 = schema.object({ + title: schema.string(), + description: schema.string({ defaultValue: '' }), + query: schema.object({ + language: schema.string(), + query: schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })]), + }), + filters: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + timefilter: schema.maybe( + schema.object({ + from: schema.string(), + to: schema.string(), + refreshInterval: schema.maybe( + schema.object({ + value: schema.number(), + pause: schema.boolean(), + }) + ), + }) + ), +}); From 823cc3f49b5cb2c91f0edc29edc1f52ab3585770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 18 Apr 2023 10:15:35 +0200 Subject: [PATCH 003/100] [`getSavedObjectsCount`] Use `soClient` instead of `.kibana` searches (#155035) ## Summary As part of #154888, we need to stop making direct requests to the index `.kibana`, and use the SO Clients instead. This PR changes the utility `getSavedObjectsCount` to use aggregations in the SO client. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) I'm pointing to `main` because it's an improvement we needed anyway. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/lib/aggregations/validation.test.ts | 17 +++ .../src/lib/aggregations/validation.ts | 8 ++ .../get_saved_object_counts.test.ts | 116 ++++++------------ .../get_saved_object_counts.ts | 44 +++---- .../kibana_usage_collector.test.ts | 12 +- .../kibana_usage_collector.ts | 11 +- .../saved_objects_count_collector.test.ts | 7 +- .../saved_objects_count_collector.ts | 5 +- .../kibana_usage_collection/server/plugin.ts | 2 +- .../kibana_usage_collection/tsconfig.json | 1 + 10 files changed, 95 insertions(+), 128 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts index 0120ecf75c797..0b5d750e57040 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.test.ts @@ -522,4 +522,21 @@ describe('validateAndConvertAggregations', () => { '"[aggName.cardinality.field] Invalid attribute path: alert.alert.actions.group"' ); }); + + it('allows aggregations for root fields', () => { + const aggregations: AggsMap = { + types: { + terms: { + field: 'type', + }, + }, + }; + expect(validateAndConvertAggregations(['foo'], aggregations, mockMappings)).toEqual({ + types: { + terms: { + field: 'type', + }, + }, + }); + }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.ts index b3a6bbae5e956..c673f73d8d844 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/aggregations/validation.ts @@ -18,6 +18,7 @@ import { rewriteRootLevelAttribute, } from './validation_utils'; import { aggregationSchemas } from './aggs_types'; +import { getRootFields } from '../included_fields'; const aggregationKeys = ['aggs', 'aggregations']; @@ -226,6 +227,10 @@ const isAttributeValue = (fieldName: string, fieldValue: unknown): boolean => { return attributeFields.includes(fieldName) && typeof fieldValue === 'string'; }; +const isRootField = (fieldName: string): boolean => { + return getRootFields().includes(fieldName); +}; + const validateAndRewriteAttributePath = ( attributePath: string, { allowedTypes, indexMapping, currentPath }: ValidationContext @@ -236,5 +241,8 @@ const validateAndRewriteAttributePath = ( if (isObjectTypeAttribute(attributePath, indexMapping, allowedTypes)) { return rewriteObjectTypeAttribute(attributePath); } + if (isRootField(attributePath)) { + return attributePath; + } throw new Error(`[${currentPath.join('.')}] Invalid attribute path: ${attributePath}`); }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.test.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.test.ts index 173453e9e2420..f83200f56f27c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.test.ts @@ -6,48 +6,41 @@ * Side Public License, v 1. */ -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks'; import { getSavedObjectsCounts } from './get_saved_object_counts'; -function mockGetSavedObjectsCounts(params: TBody) { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // @ts-expect-error arbitrary type - esClient.search.mockResponse(params); - return esClient; -} +const soEmptyResponse = { total: 0, saved_objects: [], per_page: 0, page: 1 }; describe('getSavedObjectsCounts', () => { + const fetchContextMock = createCollectorFetchContextMock(); + const soClient = fetchContextMock.soClient as jest.Mocked; + + beforeEach(() => { + soClient.find.mockReset(); + }); + test('should not fail if no body returned', async () => { - const esClient = mockGetSavedObjectsCounts({}); + soClient.find.mockResolvedValueOnce(soEmptyResponse); - const results = await getSavedObjectsCounts(esClient, '.kibana', ['type-a']); + const results = await getSavedObjectsCounts(soClient, ['type-a']); // Make sure ES.search is triggered (we'll test the actual params in other specific tests) - expect(esClient.search).toHaveBeenCalledTimes(1); + expect(soClient.find).toHaveBeenCalledTimes(1); expect(results).toStrictEqual({ total: 0, per_type: [], non_expected_types: [], others: 0 }); }); test('should match all and request the `missing` bucket (size + 1) when `exclusive === false`', async () => { - const esClient = mockGetSavedObjectsCounts({}); - await getSavedObjectsCounts(esClient, '.kibana', ['type-a', 'type_2']); - expect(esClient.search).toHaveBeenCalledWith({ - index: '.kibana', - ignore_unavailable: true, - filter_path: [ - 'aggregations.types.buckets', - 'aggregations.types.sum_other_doc_count', - 'hits.total', - ], - body: { - size: 0, - track_total_hits: true, - query: { match_all: {} }, - aggs: { - types: { - terms: { - field: 'type', - size: 3, - missing: 'missing_so_type', - }, + soClient.find.mockResolvedValueOnce(soEmptyResponse); + await getSavedObjectsCounts(soClient, ['type-a', 'type_2']); + expect(soClient.find).toHaveBeenCalledWith({ + type: ['type-a', 'type_2'], + perPage: 0, + aggs: { + types: { + terms: { + field: 'type', + size: 3, + missing: 'missing_so_type', }, }, }, @@ -55,22 +48,12 @@ describe('getSavedObjectsCounts', () => { }); test('should apply the terms query and aggregation with the size matching the length of the list when `exclusive === true`', async () => { - const esClient = mockGetSavedObjectsCounts({}); - await getSavedObjectsCounts(esClient, '.kibana', ['type_one', 'type_two'], true); - expect(esClient.search).toHaveBeenCalledWith({ - index: '.kibana', - ignore_unavailable: true, - filter_path: [ - 'aggregations.types.buckets', - 'aggregations.types.sum_other_doc_count', - 'hits.total', - ], - body: { - size: 0, - track_total_hits: true, - query: { terms: { type: ['type_one', 'type_two'] } }, - aggs: { types: { terms: { field: 'type', size: 2 } } }, - }, + soClient.find.mockResolvedValueOnce(soEmptyResponse); + await getSavedObjectsCounts(soClient, ['type_one', 'type_two'], true); + expect(soClient.find).toHaveBeenCalledWith({ + type: ['type_one', 'type_two'], + perPage: 0, + aggs: { types: { terms: { field: 'type', size: 2 } } }, }); }); @@ -80,39 +63,13 @@ describe('getSavedObjectsCounts', () => { { key: 'type-two', doc_count: 2 }, ]; - const esClient = mockGetSavedObjectsCounts({ - hits: { total: { value: 13 } }, - aggregations: { types: { buckets, sum_other_doc_count: 10 } }, - }); - - const results = await getSavedObjectsCounts(esClient, '.kibana', [ - 'type_one', - 'type-two', - 'type-3', - ]); - expect(results).toStrictEqual({ + soClient.find.mockResolvedValueOnce({ + ...soEmptyResponse, total: 13, - per_type: [ - { key: 'type_one', doc_count: 1 }, - { key: 'type-two', doc_count: 2 }, - ], - non_expected_types: [], - others: 10, - }); - }); - - test('supports ES returning total as a number (just in case)', async () => { - const buckets = [ - { key: 'type_one', doc_count: 1 }, - { key: 'type-two', doc_count: 2 }, - ]; - - const esClient = mockGetSavedObjectsCounts({ - hits: { total: 13 }, aggregations: { types: { buckets, sum_other_doc_count: 10 } }, }); - const results = await getSavedObjectsCounts(esClient, '.kibana', ['type_one', 'type-two']); + const results = await getSavedObjectsCounts(soClient, ['type_one', 'type-two', 'type-3']); expect(results).toStrictEqual({ total: 13, per_type: [ @@ -132,12 +89,13 @@ describe('getSavedObjectsCounts', () => { { key: 'type-four', doc_count: 2 }, ]; - const esClient = mockGetSavedObjectsCounts({ - hits: { total: { value: 13 } }, + soClient.find.mockResolvedValueOnce({ + ...soEmptyResponse, + total: 13, aggregations: { types: { buckets, sum_other_doc_count: 6 } }, }); - const results = await getSavedObjectsCounts(esClient, '.kibana', ['type_one', 'type-two']); + const results = await getSavedObjectsCounts(soClient, ['type_one', 'type-two']); expect(results).toStrictEqual({ total: 13, per_type: [ diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.ts index 812933eeb3e69..cdca68e5d6006 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts/get_saved_object_counts.ts @@ -7,7 +7,7 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { ElasticsearchClient } from '@kbn/core/server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; const MISSING_TYPE_KEY = 'missing_so_type'; @@ -39,40 +39,28 @@ export interface SavedObjectsCounts { * It also returns a break-down of the document count for all the built-in SOs in Kibana (or the types specified in `soTypes`). * Finally, it completes the information with an `others` counter, that indicates the number of documents that do not match the SO type breakdown. * - * @param esClient The {@link ElasticsearchClient} to use when performing the aggregation. - * @param kibanaIndex The index where SOs are stored. Typically '.kibana'. + * @param soClient The {@link SavedObjectsClientContract} to use when performing the aggregation. * @param soTypes The SO types we want to know about. * @param exclusive If `true`, the results will only contain the breakdown for the specified `soTypes`. Otherwise, it'll also return `missing` and `others` bucket. * @returns {@link SavedObjectsCounts} */ export async function getSavedObjectsCounts( - esClient: ElasticsearchClient, - kibanaIndex: string, // Typically '.kibana'. We might need a way to obtain it from the SavedObjects client (or the SavedObjects client to provide a way to run aggregations?) + soClient: SavedObjectsClientContract, soTypes: string[], exclusive: boolean = false ): Promise { - const body = await esClient.search({ - index: kibanaIndex, - ignore_unavailable: true, - filter_path: [ - 'aggregations.types.buckets', - 'aggregations.types.sum_other_doc_count', - 'hits.total', - ], - body: { - size: 0, - track_total_hits: true, - query: exclusive ? { terms: { type: soTypes } } : { match_all: {} }, - aggs: { - types: { - terms: { - field: 'type', - // If `exclusive == true`, we only care about the strict length of the provided SO types. - // Otherwise, we want to account for the `missing` bucket (size and missing option). - ...(exclusive - ? { size: soTypes.length } - : { missing: MISSING_TYPE_KEY, size: soTypes.length + 1 }), - }, + const body = await soClient.find({ + type: soTypes, + perPage: 0, + aggs: { + types: { + terms: { + field: 'type', + // If `exclusive == true`, we only care about the strict length of the provided SO types. + // Otherwise, we want to account for the `missing` bucket (size and missing option). + ...(exclusive + ? { size: soTypes.length } + : { missing: MISSING_TYPE_KEY, size: soTypes.length + 1 }), }, }, }, @@ -93,7 +81,7 @@ export async function getSavedObjectsCounts( }); return { - total: (typeof body.hits?.total === 'number' ? body.hits?.total : body.hits?.total?.value) ?? 0, + total: body.total, per_type: perType, non_expected_types: nonExpectedTypes, others: body.aggregations?.types?.sum_other_doc_count ?? 0, diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.test.ts index 65003ff99c22a..cdf2ca35d6ecc 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { Collector, createCollectorFetchContextMock, @@ -52,7 +52,8 @@ describe('kibana_usage', () => { }); describe('getKibanaSavedObjectCounts', () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const fetchContextMock = createCollectorFetchContextMock(); + const soClient = fetchContextMock.soClient; test('Get all the saved objects equal to 0 because no results were found', async () => { getSavedObjectsCountsMock.mockResolvedValueOnce({ @@ -61,7 +62,7 @@ describe('getKibanaSavedObjectCounts', () => { non_expected_types: [], others: 0, }); - const results = await getKibanaSavedObjectCounts(esClient, '.kibana'); + const results = await getKibanaSavedObjectCounts(soClient); expect(results).toStrictEqual({ dashboard: { total: 0 }, visualization: { total: 0 }, @@ -83,7 +84,7 @@ describe('getKibanaSavedObjectCounts', () => { others: 0, }); - const results = await getKibanaSavedObjectCounts(esClient, '.kibana'); + const results = await getKibanaSavedObjectCounts(soClient); expect(results).toStrictEqual({ dashboard: { total: 1 }, visualization: { total: 0 }, @@ -93,8 +94,7 @@ describe('getKibanaSavedObjectCounts', () => { }); expect(getSavedObjectsCountsMock).toHaveBeenCalledWith( - esClient, - '.kibana', + soClient, ['dashboard', 'visualization', 'search', 'index-pattern', 'graph-workspace'], true ); diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.ts index dbff86fb23ad7..9a128888f05d5 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/kibana_usage_collector.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import type { ElasticsearchClient } from '@kbn/core/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { snakeCase } from 'lodash'; +import { SavedObjectsClientContract } from '@kbn/core/server'; import { getSavedObjectsCounts } from './get_saved_object_counts'; interface KibanaSavedObjectCounts { @@ -26,10 +26,9 @@ interface KibanaUsage extends KibanaSavedObjectCounts { const TYPES = ['dashboard', 'visualization', 'search', 'index-pattern', 'graph-workspace']; export async function getKibanaSavedObjectCounts( - esClient: ElasticsearchClient, - kibanaIndex: string + soClient: SavedObjectsClientContract ): Promise { - const { per_type: buckets } = await getSavedObjectsCounts(esClient, kibanaIndex, TYPES, true); + const { per_type: buckets } = await getSavedObjectsCounts(soClient, TYPES, true); const allZeros = Object.fromEntries( TYPES.map((type) => [snakeCase(type), { total: 0 }]) @@ -80,10 +79,10 @@ export function registerKibanaUsageCollector( }, }, }, - async fetch({ esClient }) { + async fetch({ soClient }) { return { index: kibanaIndex, - ...(await getKibanaSavedObjectCounts(esClient, kibanaIndex)), + ...(await getKibanaSavedObjectCounts(soClient)), }; }, }) diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.test.ts index 25ac79f88e523..53168be3c34b6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.test.ts @@ -17,10 +17,8 @@ describe('saved_objects_count_collector', () => { const usageCollectionMock = createUsageCollectionSetupMock(); const fetchContextMock = createCollectorFetchContextMock(); - const kibanaIndex = '.kibana-tests'; - beforeAll(() => - registerSavedObjectsCountUsageCollector(usageCollectionMock, kibanaIndex, () => + registerSavedObjectsCountUsageCollector(usageCollectionMock, () => Promise.resolve(['type_one', 'type_two', 'type-three', 'type-four']) ) ); @@ -81,8 +79,7 @@ describe('saved_objects_count_collector', () => { }); expect(getSavedObjectsCountsMock).toHaveBeenCalledWith( - fetchContextMock.esClient, - kibanaIndex, + fetchContextMock.soClient, ['type_one', 'type_two', 'type-three', 'type-four'], false ); diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.ts index 376a036a0f24a..49bfb7819389d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/saved_objects_count_collector.ts @@ -23,7 +23,6 @@ interface SavedObjectsCountUsage { export function registerSavedObjectsCountUsageCollector( usageCollection: UsageCollectionSetup, - kibanaIndex: string, getAllSavedObjectTypes: () => Promise ) { usageCollection.registerCollector( @@ -68,14 +67,14 @@ export function registerSavedObjectsCountUsageCollector( }, }, }, - async fetch({ esClient }) { + async fetch({ soClient }) { const allRegisteredSOTypes = await getAllSavedObjectTypes(); const { total, per_type: buckets, non_expected_types: nonRegisteredTypes, others, - } = await getSavedObjectsCounts(esClient, kibanaIndex, allRegisteredSOTypes, false); + } = await getSavedObjectsCounts(soClient, allRegisteredSOTypes, false); return { total, by_type: buckets.map(({ key: type, doc_count: count }) => ({ type, count })), diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 784ab3ef4af62..8787d085c692e 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -148,7 +148,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { .getAllTypes() .map(({ name }) => name); }; - registerSavedObjectsCountUsageCollector(usageCollection, kibanaIndex, getAllSavedObjectTypes); + registerSavedObjectsCountUsageCollector(usageCollection, getAllSavedObjectTypes); registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); registerApplicationUsageCollector( diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index 271032000d510..d56cb860cd463 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/logging", "@kbn/core-test-helpers-kbn-server", "@kbn/core-usage-data-server", + "@kbn/core-saved-objects-api-server", ], "exclude": [ "target/**/*", From 617debdf79a9af77164e89755f4d2defdd72fafa Mon Sep 17 00:00:00 2001 From: Sean Story Date: Tue, 18 Apr 2023 03:42:26 -0500 Subject: [PATCH 004/100] Generate the sample doc differently for sourceField vs fieldMappings (#154980) ## Summary Generates the sample document for testing your pipeline differently depending on if you are working with `sourceField` and `destinationField` vs if you're using a `FieldMapping[]`. Before: Screenshot 2023-04-14 at 2 05 58 PM After: Screenshot 2023-04-14 at 2 06 06 PM ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../pipelines/ml_inference/test_pipeline.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx index 2e7fbd51e9147..5d6332b7c0475 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx @@ -34,7 +34,7 @@ import './add_inference_pipeline_flyout.scss'; export const TestPipeline: React.FC = () => { const { addInferencePipelineModal: { - configuration: { sourceField }, + configuration: { sourceField, fieldMappings }, indexName, }, getDocumentsErr, @@ -49,6 +49,12 @@ export const TestPipeline: React.FC = () => { const isSmallerViewport = useIsWithinMaxBreakpoint('s'); const inputRef = useRef(); + const sampleFieldValue = i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.sampleValue', + { + defaultMessage: 'REPLACE ME', + } + ); return ( <> @@ -152,7 +158,22 @@ export const TestPipeline: React.FC = () => {

- {`[{"_index":"index","_id":"id","_source":{"${sourceField}":"bar"}}]`} + {JSON.stringify( + JSON.parse( + `[{"_index":"index", "_id":"id", "_source":{${ + fieldMappings + ? fieldMappings + .map( + (fieldMapping) => + `"${fieldMapping.sourceField}": "${sampleFieldValue}"` + ) + .join(', ') + : `"${sourceField}":"${sampleFieldValue}"` + }}}]` + ), + null, + 2 + )}
From c75863385d374b2716972f67e60f67256c899bf8 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 18 Apr 2023 11:03:44 +0200 Subject: [PATCH 005/100] Use Observability Page Template from Observability Shared in APM and Profiling (#154776) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/packages/ml/agg_utils/tsconfig.json | 2 +- x-pack/plugins/apm/kibana.jsonc | 6 ++---- x-pack/plugins/apm/public/application/index.tsx | 1 + .../apm/public/components/routing/apm_error_boundary.tsx | 4 ++-- .../components/routing/templates/apm_main_template.tsx | 6 +++--- .../routing/templates/settings_template.stories.tsx | 2 +- .../public/context/apm_plugin/mock_apm_plugin_context.tsx | 1 + x-pack/plugins/apm/public/plugin.ts | 8 +++++++- x-pack/plugins/apm/tsconfig.json | 1 + x-pack/plugins/profiling/kibana.jsonc | 6 ++---- x-pack/plugins/profiling/public/plugin.tsx | 2 +- x-pack/plugins/profiling/public/types.ts | 2 ++ x-pack/plugins/profiling/tsconfig.json | 1 + 13 files changed, 25 insertions(+), 17 deletions(-) diff --git a/x-pack/packages/ml/agg_utils/tsconfig.json b/x-pack/packages/ml/agg_utils/tsconfig.json index 53328adab80f2..fc8993c828529 100644 --- a/x-pack/packages/ml/agg_utils/tsconfig.json +++ b/x-pack/packages/ml/agg_utils/tsconfig.json @@ -17,7 +17,7 @@ "@kbn/ml-is-populated-object", "@kbn/ml-string-hash", "@kbn/data-views-plugin", - "@kbn/ml-random-sampler-utils" + "@kbn/ml-random-sampler-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/apm/kibana.jsonc b/x-pack/plugins/apm/kibana.jsonc index 02ca85e30f0ad..f8038cad8fff7 100644 --- a/x-pack/plugins/apm/kibana.jsonc +++ b/x-pack/plugins/apm/kibana.jsonc @@ -7,10 +7,7 @@ "id": "apm", "server": true, "browser": true, - "configPath": [ - "xpack", - "apm" - ], + "configPath": ["xpack", "apm"], "requiredPlugins": [ "data", "embeddable", @@ -19,6 +16,7 @@ "inspector", "licensing", "observability", + "observabilityShared", "exploratoryView", "ruleRegistry", "triggersActionsUi", diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 5711b5a1f41c8..9bad342c7669e 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ({ data: pluginsStart.data, inspector: pluginsStart.inspector, observability: pluginsStart.observability, + observabilityShared: pluginsStart.observabilityShared, observabilityRuleTypeRegistry, dataViews: pluginsStart.dataViews, unifiedSearch: pluginsStart.unifiedSearch, diff --git a/x-pack/plugins/apm/public/components/routing/apm_error_boundary.tsx b/x-pack/plugins/apm/public/components/routing/apm_error_boundary.tsx index 26b641a25896d..3b7849ddacf30 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_error_boundary.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_error_boundary.tsx @@ -45,9 +45,9 @@ const pageHeader = { function ErrorWithTemplate({ error }: { error: Error }) { const { services } = useKibana(); - const { observability } = services; + const { observabilityShared } = services; - const ObservabilityPageTemplate = observability.navigation.PageTemplate; + const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; if (error instanceof NotFoundRouteException) { return ( diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index c4bcc4e5fc612..5704fd18058bd 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -7,7 +7,7 @@ import { EuiPageHeaderProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { ObservabilityPageTemplateProps } from '@kbn/observability-plugin/public/components/shared/page_template/page_template'; +import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import React from 'react'; import { useLocation } from 'react-router-dom'; @@ -54,10 +54,10 @@ export function ApmMainTemplate({ const location = useLocation(); const { services } = useKibana(); - const { http, docLinks, observability, application } = services; + const { http, docLinks, observabilityShared, application } = services; const basePath = http?.basePath.get(); - const ObservabilityPageTemplate = observability.navigation.PageTemplate; + const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; const { data, status } = useFetcher((callApmApi) => { return callApmApi('GET /internal/apm/has_data'); diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx index ec3188f4ff197..b214a79b7a1f8 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx @@ -16,7 +16,7 @@ import { SettingsTemplate } from './settings_template'; type Args = ComponentProps; const coreMock = { - observability: { + observabilityShared: { navigation: { PageTemplate: () => { return <>hello world; diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index ffca2a1b3cc4a..6924277f229c0 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -94,6 +94,7 @@ const mockCorePlugins = { inspector: {}, maps: {}, observability: {}, + observabilityShared: {}, data: {}, }; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index ba7b455c05bb6..ac1d7294a27be 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -38,6 +38,10 @@ import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/public'; import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/public'; import { FetchDataParams, METRIC_TYPE, @@ -80,6 +84,7 @@ export interface ApmPluginSetupDeps { licensing: LicensingPluginSetup; ml?: MlPluginSetup; observability: ObservabilityPublicSetup; + observabilityShared: ObservabilitySharedPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; share: SharePluginSetup; } @@ -96,6 +101,7 @@ export interface ApmPluginStartDeps { ml?: MlPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; + observabilityShared: ObservabilitySharedPluginStart; fleet?: FleetStart; fieldFormats?: FieldFormatsStart; security?: SecurityPluginStart; @@ -162,7 +168,7 @@ export class ApmPlugin implements Plugin { } // register observability nav if user has access to plugin - plugins.observability.navigation.registerSections( + plugins.observabilityShared.navigation.registerSections( from(core.getStartServices()).pipe( map(([coreStart, pluginsStart]) => { if (coreStart.application.capabilities.apm.show) { diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 5419244c7cb15..e049bed128afa 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -83,6 +83,7 @@ "@kbn/alerts-as-data-utils", "@kbn/exploratory-view-plugin", "@kbn/logging-mocks", + "@kbn/observability-shared-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc index a1290cc79b499..bf7f17fb5e5ec 100644 --- a/x-pack/plugins/profiling/kibana.jsonc +++ b/x-pack/plugins/profiling/kibana.jsonc @@ -6,16 +6,14 @@ "id": "profiling", "server": true, "browser": true, - "configPath": [ - "xpack", - "profiling" - ], + "configPath": ["xpack", "profiling"], "requiredPlugins": [ "navigation", "data", "kibanaUtils", "share", "observability", + "observabilityShared", "features", "kibanaReact", "unifiedSearch", diff --git a/x-pack/plugins/profiling/public/plugin.tsx b/x-pack/plugins/profiling/public/plugin.tsx index ae3030b528efb..4d700d1b028fb 100644 --- a/x-pack/plugins/profiling/public/plugin.tsx +++ b/x-pack/plugins/profiling/public/plugin.tsx @@ -77,7 +77,7 @@ export class ProfilingPlugin implements Plugin { }) ); - pluginsSetup.observability.navigation.registerSections(section$); + pluginsSetup.observabilityShared.navigation.registerSections(section$); coreSetup.application.register({ id: 'profiling', diff --git a/x-pack/plugins/profiling/public/types.ts b/x-pack/plugins/profiling/public/types.ts index 42563dd3258fb..da0bc65f02d34 100644 --- a/x-pack/plugins/profiling/public/types.ts +++ b/x-pack/plugins/profiling/public/types.ts @@ -13,11 +13,13 @@ import type { ObservabilityPublicSetup, ObservabilityPublicStart, } from '@kbn/observability-plugin/public'; +import { ObservabilitySharedPluginSetup } from '@kbn/observability-shared-plugin/public/plugin'; import { ChartsPluginSetup, ChartsPluginStart } from '@kbn/charts-plugin/public'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; export interface ProfilingPluginPublicSetupDeps { observability: ObservabilityPublicSetup; + observabilityShared: ObservabilitySharedPluginSetup; dataViews: DataViewsPublicPluginSetup; data: DataPublicPluginSetup; charts: ChartsPluginSetup; diff --git a/x-pack/plugins/profiling/tsconfig.json b/x-pack/plugins/profiling/tsconfig.json index 4002050ee8e79..effbfc83a3716 100644 --- a/x-pack/plugins/profiling/tsconfig.json +++ b/x-pack/plugins/profiling/tsconfig.json @@ -44,6 +44,7 @@ "@kbn/i18n-react", "@kbn/ml-plugin", "@kbn/share-plugin", + "@kbn/observability-shared-plugin", "@kbn/licensing-plugin", // add references to other TypeScript projects the plugin depends on From 9d096c5623066fbd8464d61cd745fbea8a1692bf Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 18 Apr 2023 11:31:21 +0200 Subject: [PATCH 006/100] [Lens] Improve reporting size for Lens visualizations (#154931) ## Summary Fix #154894 This PR "rotate" most of Lens visualizations to have a landscape version of the PNG/PDF exports. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../plugins/lens/public/app_plugin/lens_top_nav.tsx | 12 +++++++++++- .../plugins/lens/public/app_plugin/share_action.ts | 8 ++++++++ x-pack/plugins/lens/public/types.ts | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 2678b5a0368fb..50d70bb58ae75 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -37,7 +37,7 @@ import { import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; import { changeIndexPattern } from '../state_management/lens_slice'; import { LensByReferenceInput } from '../embeddable'; -import { getShareURL } from './share_action'; +import { DEFAULT_LENS_LAYOUT_DIMENSIONS, getShareURL } from './share_action'; function getSaveButtonMeta({ contextFromEmbeddable, @@ -578,6 +578,10 @@ export const LensTopNavMenu = ({ return; } + if (visualization.activeId == null || !visualizationMap[visualization.activeId]) { + return; + } + const { shareableUrl, savedObjectURL, @@ -608,6 +612,12 @@ export const LensTopNavMenu = ({ id: LENS_APP_LOCATOR, params: locatorParams, }, + layout: { + dimensions: + visualizationMap[visualization.activeId].getReportingLayout?.( + visualization.state + ) ?? DEFAULT_LENS_LAYOUT_DIMENSIONS, + }, }; share.toggleShareContextMenu({ diff --git a/x-pack/plugins/lens/public/app_plugin/share_action.ts b/x-pack/plugins/lens/public/app_plugin/share_action.ts index 55e4b978f015d..c9ec3a11ef5e7 100644 --- a/x-pack/plugins/lens/public/app_plugin/share_action.ts +++ b/x-pack/plugins/lens/public/app_plugin/share_action.ts @@ -27,6 +27,14 @@ interface ShareableConfiguration adHocDataViews?: DataViewSpec[]; } +// This approximate Lens workspace dimensions ratio on a typical widescreen +export const DEFAULT_LENS_LAYOUT_DIMENSIONS = { + width: 1793, + // this is a magic number from the reporting tool implementation + // see: x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts#L146 + height: 1086, +}; + function getShareURLForSavedObject( { application, data }: Pick, currentDoc: Document | undefined diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 786d75816cafa..786fc50450a94 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -1292,6 +1292,10 @@ export interface Visualization { ) => Suggestion | undefined; getVisualizationInfo?: (state: T) => VisualizationInfo; + /** + * A visualization can return custom dimensions for the reporting tool + */ + getReportingLayout?: (state: T) => { height: number; width: number }; } // Use same technique as TriggerContext From 779e553f811477a9454783ab14e3596bfa508431 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 18 Apr 2023 11:31:37 +0200 Subject: [PATCH 007/100] [Lens] Improve Ignore global filters UI (#154441) ## Summary Fixes #154316 This PR revisits partially the Layer Settings UI in order to support the `Data` and `Appereance` split in the UI and enhance the `ignore global filters` UI to make it clearer for the user the state of this setting. Screenshot 2023-04-05 at 11 57 55 when the setting is disabled (to use global filters) then nothing is shown: Screenshot 2023-04-05 at 11 59 18 On Annotations layer that is the only available setting: Screenshot 2023-04-05 at 11 58 03 The new refactoring can now handle multiple settings for each panel section as shown in the Partition charts settings panel: Screenshot 2023-04-05 at 12 20 35 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Stratoula Kalafateli --- .../datasources/form_based/form_based.tsx | 6 +- .../datasources/form_based/layer_settings.tsx | 161 ++++++++---------- .../datasources/form_based/layerpanel.tsx | 25 ++- .../form_based}/sampling_icon.tsx | 0 .../editor_frame/config_panel/layer_panel.tsx | 62 ++++++- .../dataview_picker/dataview_picker.tsx | 103 +---------- .../dataview_picker/trigger.tsx | 101 +++++++++++ x-pack/plugins/lens/public/types.ts | 4 +- .../partition/layer_settings.test.tsx | 9 +- .../partition/layer_settings.tsx | 59 ++++--- .../partition/visualization.test.ts | 21 ++- .../partition/visualization.tsx | 2 +- .../visualizations/xy/annotations/actions.ts | 35 +--- .../visualizations/xy/layer_settings.tsx | 53 ++++++ .../visualizations/xy/visualization.test.ts | 98 ++++++----- .../visualizations/xy/visualization.tsx | 47 +++-- .../xy/xy_config_panel/layer_header.tsx | 31 +++- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../apps/lens/group1/layer_actions.ts | 14 +- 21 files changed, 488 insertions(+), 355 deletions(-) rename x-pack/plugins/lens/public/{shared_components/dataview_picker => datasources/form_based}/sampling_icon.tsx (100%) create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/xy/layer_settings.tsx diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 4152212e51fea..dbe6fcafd0ed0 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -37,7 +37,6 @@ import type { IndexPatternField, IndexPattern, IndexPatternRef, - DatasourceLayerSettingsProps, DataSourceInfo, UserMessage, FrameDatasourceAPI, @@ -429,10 +428,7 @@ export function getFormBasedDatasource({ toExpression: (state, layerId, indexPatterns, dateRange, searchSessionId) => toExpression(state, layerId, indexPatterns, uiSettings, dateRange, searchSessionId), - renderLayerSettings( - domElement: Element, - props: DatasourceLayerSettingsProps - ) { + renderLayerSettings(domElement, props) { render( diff --git a/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx b/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx index 38da5475e22e1..c79bc41dd7dd7 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx @@ -109,101 +109,86 @@ export function LayerSettingsPanel({ setState, layerId, }: DatasourceLayerSettingsProps) { - const { euiTheme } = useEuiTheme(); const isSamplingValueDisabled = !isSamplingValueEnabled(state.layers[layerId]); const currentValue = isSamplingValueDisabled ? samplingValues[samplingValues.length - 1] : state.layers[layerId].sampling; return ( -
- -

- {i18n.translate('xpack.lens.indexPattern.layerSettings.headingData', { - defaultMessage: 'Data', - })} -

-
- - -

- - - - ), - }} - /> -

- - } - label={ - <> - {i18n.translate('xpack.lens.indexPattern.randomSampling.label', { - defaultMessage: 'Sampling', - })}{' '} - + +

+ + + + ), + }} + /> +

+ + } + label={ + <> + {i18n.translate('xpack.lens.indexPattern.randomSampling.label', { + defaultMessage: 'Sampling', + })}{' '} + + - - - - } - > - { - setState({ - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], - sampling: newSamplingValue, - }, + size="s" + /> +
+ + } + > + { + setState({ + ...state, + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + sampling: newSamplingValue, }, - }); - }} - /> -
-
+ }, + }); + }} + /> + ); } diff --git a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx index 1de4d0844245f..5832f8094a856 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx @@ -8,10 +8,12 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { useEuiTheme } from '@elastic/eui'; import { DatasourceLayerPanelProps } from '../../types'; import { FormBasedPrivateState } from './types'; import { ChangeIndexPattern } from '../../shared_components/dataview_picker/dataview_picker'; import { getSamplingValue } from './utils'; +import { RandomSamplingIcon } from './sampling_icon'; export interface FormBasedLayerPanelProps extends DatasourceLayerPanelProps { state: FormBasedPrivateState; @@ -25,6 +27,7 @@ export function LayerPanel({ dataViews, }: FormBasedLayerPanelProps) { const layer = state.layers[layerId]; + const { euiTheme } = useEuiTheme(); const indexPattern = dataViews.indexPatterns[layer.indexPatternId]; const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { @@ -38,6 +41,26 @@ export function LayerPanel({ }; }); + const samplingValue = getSamplingValue(layer); + const extraIconLabelProps = + samplingValue !== 1 + ? { + icon: { + component: ( + + ), + value: `${samplingValue * 100}%`, + tooltipValue: i18n.translate('xpack.lens.indexPattern.randomSamplingInfo', { + defaultMessage: '{value}% sampling', + values: { + value: samplingValue * 100, + }, + }), + 'data-test-subj': 'lnsChangeIndexPatternSamplingInfo', + }, + } + : {}; + return ( + activeVisualization.hasLayerSettings?.({ + layerId, + state: visualizationState, + frame: props.framePublicAPI, + }) || { data: false, appearance: false }, + [activeVisualization, layerId, props.framePublicAPI, visualizationState] + ); + const compatibleActions = useMemo( () => [ @@ -341,11 +351,7 @@ export function LayerPanel( isOnlyLayer, isTextBasedLanguage, hasLayerSettings: Boolean( - (activeVisualization.hasLayerSettings?.({ - layerId, - state: visualizationState, - frame: props.framePublicAPI, - }) && + (Object.values(visualizationLayerSettings).some(Boolean) && activeVisualization.renderLayerSettings) || layerDatasource?.renderLayerSettings ), @@ -364,8 +370,8 @@ export function LayerPanel( layerIndex, onCloneLayer, onRemoveLayer, - props.framePublicAPI, updateVisualization, + visualizationLayerSettings, visualizationState, ] ); @@ -682,15 +688,56 @@ export function LayerPanel( >
+ {layerDatasource?.renderLayerSettings || visualizationLayerSettings.data ? ( + +

+ {i18n.translate('xpack.lens.editorFrame.layerSettings.headingData', { + defaultMessage: 'Data', + })} +

+
+ ) : null} {layerDatasource?.renderLayerSettings && ( <> - )} + {layerDatasource?.renderLayerSettings && visualizationLayerSettings.data ? ( + + ) : null} + {activeVisualization?.renderLayerSettings && visualizationLayerSettings.data ? ( + + ) : null} + {visualizationLayerSettings.appearance ? ( + +

+ {i18n.translate('xpack.lens.editorFrame.layerSettings.headingAppearance', { + defaultMessage: 'Appearance', + })} +

+
+ ) : null} {activeVisualization?.renderLayerSettings && ( )} diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index 6467cbcb58494..9d55284cc36c8 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -7,109 +7,10 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, - EuiSelectableProps, - EuiTextColor, - EuiToolTip, - useEuiTheme, -} from '@elastic/eui'; +import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; -import { css } from '@emotion/react'; import { type IndexPatternRef } from '../../types'; -import { type ToolbarButtonProps, ToolbarButton } from './toolbar_button'; -import { RandomSamplingIcon } from './sampling_icon'; - -export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { - label: string; - title?: string; - isDisabled?: boolean; - samplingValue?: number; -}; - -function TriggerButton({ - label, - title, - togglePopover, - isMissingCurrent, - samplingValue, - ...rest -}: ChangeIndexPatternTriggerProps & - ToolbarButtonProps & { - togglePopover: () => void; - isMissingCurrent?: boolean; - }) { - const { euiTheme } = useEuiTheme(); - // be careful to only add color with a value, otherwise it will fallbacks to "primary" - const colorProp = isMissingCurrent - ? { - color: 'danger' as const, - } - : {}; - const content = - samplingValue != null && samplingValue !== 1 ? ( - - - {label} - - - - - - - - - - {samplingValue * 100}% - - - - - - - ) : ( - label - ); - return ( - togglePopover()} - fullWidth - {...colorProp} - {...rest} - textProps={{ style: { width: '100%' } }} - > - {content} - - ); -} +import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger'; export function ChangeIndexPattern({ indexPatternRefs, diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.tsx new file mode 100644 index 0000000000000..038b1d5aec960 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/trigger.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEuiTheme, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiTextColor } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; +import { ToolbarButton, ToolbarButtonProps } from './toolbar_button'; + +interface TriggerLabelProps { + label: string; + icon?: { + component: React.ReactElement; + value?: string; + tooltipValue?: string; + 'data-test-subj': string; + }; +} + +export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & + TriggerLabelProps & { + label: string; + title?: string; + isDisabled?: boolean; + }; + +function TriggerLabel({ label, icon }: TriggerLabelProps) { + const { euiTheme } = useEuiTheme(); + if (!icon) { + return <>{label}; + } + return ( + + + {label} + + + + + {icon.component} + {icon.value ? ( + + {icon.value} + + ) : null} + + + + + ); +} + +export function TriggerButton({ + label, + title, + togglePopover, + isMissingCurrent, + icon, + ...rest +}: ChangeIndexPatternTriggerProps & { + togglePopover: () => void; + isMissingCurrent?: boolean; +}) { + // be careful to only add color with a value, otherwise it will fallbacks to "primary" + const colorProp = isMissingCurrent + ? { + color: 'danger' as const, + } + : {}; + return ( + togglePopover()} + fullWidth + {...colorProp} + {...rest} + textProps={{ style: { width: '100%' } }} + > + + + ); +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 786fc50450a94..faa3d8e14e5bd 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -1189,11 +1189,11 @@ export interface Visualization { /** * Allows the visualization to announce whether or not it has any settings to show */ - hasLayerSettings?: (props: VisualizationConfigProps) => boolean; + hasLayerSettings?: (props: VisualizationConfigProps) => Record<'data' | 'appearance', boolean>; renderLayerSettings?: ( domElement: Element, - props: VisualizationLayerSettingsProps + props: VisualizationLayerSettingsProps & { section: 'data' | 'appearance' } ) => ((cleanupElement: Element) => void) | void; /** diff --git a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx index 9acedbc9b8dd2..2fed483cddfaf 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx @@ -25,12 +25,15 @@ describe('layer settings', () => { }); const layerId = 'layer-id'; - const props: VisualizationLayerSettingsProps = { + const props: VisualizationLayerSettingsProps & { + section: 'data' | 'appearance'; + } = { setState: jest.fn(), layerId, state: getState(false), frame: {} as FramePublicAPI, panelRef: {} as React.MutableRefObject, + section: 'data', }; it('toggles multiple metrics', () => { @@ -90,5 +93,9 @@ describe('layer settings', () => { ).isEmptyRender() ).toBeTruthy(); }); + + test('should not render anything for the appearance section', () => { + expect(shallow().isEmptyRender()); + }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx index 39da4fbfe0595..6f13800a91a2c 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx @@ -12,7 +12,12 @@ import { PieChartTypes } from '../../../common/constants'; import { PieVisualizationState } from '../..'; import { VisualizationLayerSettingsProps } from '../../types'; -export function LayerSettings(props: VisualizationLayerSettingsProps) { +export function LayerSettings( + props: VisualizationLayerSettingsProps & { section: 'data' | 'appearance' } +) { + if (props.section === 'appearance') { + return null; + } if (props.state.shape === PieChartTypes.MOSAIC) { return null; } @@ -24,29 +29,33 @@ export function LayerSettings(props: VisualizationLayerSettingsProps - - { - props.setState({ - ...props.state, - layers: props.state.layers.map((layer) => - layer.layerId !== props.layerId - ? layer - : { - ...layer, - allowMultipleMetrics: !layer.allowMultipleMetrics, - } - ), - }); - }} - /> - - + + { + props.setState({ + ...props.state, + layers: props.state.layers.map((layer) => + layer.layerId !== props.layerId + ? layer + : { + ...layer, + allowMultipleMetrics: !layer.allowMultipleMetrics, + } + ), + }); + }} + /> + ); } diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index 02932a0aa5e1c..3a4b0545a42ea 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -617,10 +617,10 @@ describe('pie_visualization', () => { }); }); - it.each(Object.values(PieChartTypes).filter((type) => type !== 'mosaic'))( + it.each(Object.values(PieChartTypes).filter((type) => type !== PieChartTypes.MOSAIC))( '%s adds fake dimension', (type) => { - const state = { ...getExampleState(), type }; + const state = { ...getExampleState(), shape: type }; state.layers[0].metrics.push('1', '2'); state.layers[0].allowMultipleMetrics = true; expect( @@ -645,4 +645,21 @@ describe('pie_visualization', () => { } ); }); + + describe('layer settings', () => { + describe('hasLayerSettings', () => { + it('should have data settings for all partition chart types but mosaic', () => { + for (const type of Object.values(PieChartTypes)) { + const state = { ...getExampleState(), shape: type }; + expect( + pieVisualization.hasLayerSettings?.({ + state, + frame: mockFrame(), + layerId: state.layers[0].layerId, + }) + ).toEqual({ data: type !== PieChartTypes.MOSAIC, appearance: false }); + } + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 8340b5a5e254b..da52c6efc105b 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -504,7 +504,7 @@ export const getPieVisualization = ({ }, hasLayerSettings(props) { - return props.state.shape !== 'mosaic'; + return { data: props.state.shape !== PieChartTypes.MOSAIC, appearance: false }; }, renderLayerSettings(domElement, props) { diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts index 68938fbee5211..cb3afbcb14c91 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts @@ -5,13 +5,10 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { LayerActionFromVisualization } from '../../../types'; import type { XYState, XYAnnotationLayerConfig } from '../types'; -export const IGNORE_GLOBAL_FILTERS_ACTION_ID = 'ignoreGlobalFilters'; -export const KEEP_GLOBAL_FILTERS_ACTION_ID = 'keepGlobalFilters'; - +// Leaving the stub for annotation groups export const createAnnotationActions = ({ state, layer, @@ -21,33 +18,5 @@ export const createAnnotationActions = ({ layer: XYAnnotationLayerConfig; layerIndex: number; }): LayerActionFromVisualization[] => { - const label = !layer.ignoreGlobalFilters - ? i18n.translate('xpack.lens.xyChart.annotations.ignoreGlobalFiltersLabel', { - defaultMessage: 'Ignore global filters', - }) - : i18n.translate('xpack.lens.xyChart.annotations.keepGlobalFiltersLabel', { - defaultMessage: 'Keep global filters', - }); - return [ - { - id: !layer.ignoreGlobalFilters - ? IGNORE_GLOBAL_FILTERS_ACTION_ID - : KEEP_GLOBAL_FILTERS_ACTION_ID, - displayName: label, - description: !layer.ignoreGlobalFilters - ? i18n.translate('xpack.lens.xyChart.annotations.ignoreGlobalFiltersDescription', { - defaultMessage: - 'All the dimensions configured in this layer ignore filters defined at kibana level.', - }) - : i18n.translate('xpack.lens.xyChart.annotations.keepGlobalFiltersDescription', { - defaultMessage: - 'All the dimensions configured in this layer respect filters defined at kibana level.', - }), - icon: !layer.ignoreGlobalFilters ? 'filterIgnore' : 'filter', - isCompatible: true, - 'data-test-subj': !layer.ignoreGlobalFilters - ? 'lnsXY_annotationLayer_ignoreFilters' - : 'lnsXY_annotationLayer_keepFilters', - }, - ]; + return []; }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/layer_settings.tsx b/x-pack/plugins/lens/public/visualizations/xy/layer_settings.tsx new file mode 100644 index 0000000000000..55091aa0d1d40 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/xy/layer_settings.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import type { VisualizationLayerSettingsProps } from '../../types'; +import type { XYState } from './types'; +import { isAnnotationsLayer } from './visualization_helpers'; + +export function LayerSettings({ + state, + setState, + section, + layerId, +}: VisualizationLayerSettingsProps & { section: 'data' | 'appearance' }) { + if (section === 'appearance') { + return null; + } + const layer = state.layers.find((l) => l.layerId === layerId); + if (!layer || !isAnnotationsLayer(layer)) { + return null; + } + return ( + + { + const layerIndex = state.layers.findIndex((l) => l === layer); + const newLayer = { ...layer, ignoreGlobalFilters: !layer.ignoreGlobalFilters }; + const newLayers = [...state.layers]; + newLayers[layerIndex] = newLayer; + setState({ ...state, layers: newLayers }); + }} + compressed + /> + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 159014b043aec..9b82ab8c0d90e 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -37,7 +37,6 @@ import { DataViewsState } from '../../state_management'; import { createMockedIndexPattern } from '../../datasources/form_based/mocks'; import { createMockDataViewsState } from '../../data_views_service/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { KEEP_GLOBAL_FILTERS_ACTION_ID } from './annotations/actions'; import { layerTypes, Visualization } from '../..'; const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column'; @@ -3022,7 +3021,7 @@ describe('xy_visualization', () => { ); }); - it('should return one action for an annotation layer', () => { + it('should return no action for an annotation layer', () => { const baseState = exampleState(); expect( xyVisualization.getSupportedActionsForLayer?.('annotation', { @@ -3038,53 +3037,64 @@ describe('xy_visualization', () => { }, ], }) - ).toEqual([ - expect.objectContaining({ - displayName: 'Keep global filters', - description: - 'All the dimensions configured in this layer respect filters defined at kibana level.', - icon: 'filter', - isCompatible: true, - 'data-test-subj': 'lnsXY_annotationLayer_keepFilters', - }), - ]); + ).toHaveLength(0); }); + }); - it('should handle an annotation action', () => { - const baseState = exampleState(); - const state = { - ...baseState, - layers: [ - ...baseState.layers, - { - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, - indexPatternId: 'myIndexPattern', - }, - ], - }; + describe('layer settings', () => { + describe('hasLayerSettings', () => { + it('should expose no settings for a data or reference lines layer', () => { + const baseState = exampleState(); + expect( + xyVisualization.hasLayerSettings?.({ + state: baseState, + frame: createMockFramePublicAPI(), + layerId: 'first', + }) + ).toEqual({ data: false, appearance: false }); - const newState = xyVisualization.onLayerAction!( - 'annotation', - KEEP_GLOBAL_FILTERS_ACTION_ID, - state - ); + expect( + xyVisualization.hasLayerSettings?.({ + state: { + ...baseState, + layers: [ + ...baseState.layers, + { + layerId: 'referenceLine', + layerType: layerTypes.REFERENCELINE, + accessors: [], + yConfig: [{ axisMode: 'left', forAccessor: 'a' }], + }, + ], + }, + frame: createMockFramePublicAPI(), + layerId: 'referenceLine', + }) + ).toEqual({ data: false, appearance: false }); + }); - expect(newState).toEqual( - expect.objectContaining({ - layers: expect.arrayContaining([ - { - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - annotations: [exampleAnnotation2], - ignoreGlobalFilters: false, - indexPatternId: 'myIndexPattern', + it('should expose data settings for an annotation layer', () => { + const baseState = exampleState(); + expect( + xyVisualization.hasLayerSettings?.({ + state: { + ...baseState, + layers: [ + ...baseState.layers, + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + indexPatternId: 'myIndexPattern', + }, + ], }, - ]), - }) - ); + frame: createMockFramePublicAPI(), + layerId: 'annotation', + }) + ).toEqual({ data: true, appearance: false }); + }); }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 274b82e5a1cf4..2f5df56c7b42d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -49,7 +49,6 @@ import { type XYDataLayerConfig, type SeriesType, type PersistedState, - type XYAnnotationLayerConfig, visualizationTypes, } from './types'; import { @@ -103,12 +102,8 @@ import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { DimensionTrigger } from '../../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; -import { - createAnnotationActions, - IGNORE_GLOBAL_FILTERS_ACTION_ID, - KEEP_GLOBAL_FILTERS_ACTION_ID, -} from './annotations/actions'; import { IgnoredGlobalFiltersEntries } from './info_badges'; +import { LayerSettings } from './layer_settings'; const XY_ID = 'lnsXY'; export const getXyVisualization = ({ @@ -263,31 +258,27 @@ export const getXyVisualization = ({ ]; }, - getSupportedActionsForLayer(layerId, state) { - const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); - const layer = state.layers[layerIndex]; - const actions = []; - if (isAnnotationsLayer(layer)) { - actions.push(...createAnnotationActions({ state, layerIndex, layer })); - } - return actions; + getSupportedActionsForLayer() { + return []; }, - onLayerAction(layerId, actionId, state) { - if ([IGNORE_GLOBAL_FILTERS_ACTION_ID, KEEP_GLOBAL_FILTERS_ACTION_ID].includes(actionId)) { - return { - ...state, - layers: state.layers.map((layer) => - layer.layerId === layerId - ? { - ...layer, - ignoreGlobalFilters: !(layer as XYAnnotationLayerConfig).ignoreGlobalFilters, - } - : layer - ), - }; - } + hasLayerSettings({ state, layerId: currentLayerId }) { + const layer = state.layers?.find(({ layerId }) => layerId === currentLayerId); + return { data: Boolean(layer && isAnnotationsLayer(layer)), appearance: false }; + }, + + renderLayerSettings(domElement, props) { + render( + + + + + , + domElement + ); + }, + onLayerAction(layerId, actionId, state) { return state; }, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 819dfe13c2ba2..1983094f483b9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -7,9 +7,17 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; +import { + EuiIcon, + EuiPopover, + EuiSelectable, + EuiText, + EuiPopoverTitle, + useEuiTheme, +} from '@elastic/eui'; import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; import { IconChartBarReferenceLine, IconChartBarAnnotations } from '@kbn/chart-icons'; +import { css } from '@emotion/react'; import type { VisualizationLayerHeaderContentProps, VisualizationLayerWidgetProps, @@ -71,6 +79,7 @@ function AnnotationLayerHeaderContent({ layerId, onChangeIndexPattern, }: VisualizationLayerHeaderContentProps) { + const { euiTheme } = useEuiTheme(); const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', }); @@ -78,6 +87,25 @@ function AnnotationLayerHeaderContent({ const layer = state.layers[layerIndex] as XYAnnotationLayerConfig; const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; + const extraIconLabelProps = !layer.ignoreGlobalFilters + ? {} + : { + icon: { + component: ( + + ), + tooltipValue: i18n.translate('xpack.lens.layerPanel.ignoreGlobalFilters', { + defaultMessage: 'Ignore global filters', + }), + 'data-test-subj': 'lnsChangeIndexPatternIgnoringFilters', + }, + }; return ( { + it('should add an annotation layer and settings shoud be available with ignore filters', async () => { // configure a date histogram await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -65,10 +65,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // add annotation layer await PageObjects.lens.createLayer('annotations'); + + expect(await testSubjects.exists('lnsChangeIndexPatternIgnoringFilters')).to.be(true); + await PageObjects.lens.openLayerContextMenu(1); - await testSubjects.existOrFail('lnsXY_annotationLayer_keepFilters'); - // layer settings not available - await testSubjects.missingOrFail('lnsLayerSettings'); + await testSubjects.click('lnsLayerSettings'); + // annotations settings have only ignore filters + await testSubjects.click('lnsXY-layerSettings-ignoreGlobalFilters'); + // now close the panel and check the dataView picker has no icon + await testSubjects.click('lns-indexPattern-dimensionContainerBack'); + expect(await testSubjects.exists('lnsChangeIndexPatternIgnoringFilters')).to.be(false); }); it('should add a new visualization layer and disable the sampling if max operation is chosen', async () => { From e472ed5fbac5c13e669274365daac39ce20e627a Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Tue, 18 Apr 2023 12:03:24 +0200 Subject: [PATCH 008/100] [Logs UI]: Fix log error on fetching previous entries (#154459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes #151700 As shown in the video on [this comment](https://github.com/elastic/kibana/issues/151700#issuecomment-1497246729), the issue was reproduced when jumping to the lowest possible edge of the available logs in the lateral timeline. In case there are no existing logs before the selected time cursor, the `topCursor` value required for the previous logs call is set to `null` and the error was triggered. I believe in this case the conditional to trigger the error should not only rely on the missing `topCursor` variable existence, but we should also check whether previous logs exist with the `hasMoreBefore` state value. These are the conditionals in order of execution: - Top cursor missing but has more previous entries ➡️ trigger the Error - No more previous entries and the request is not forced ➡️ Short circuit data fetching - Top cursor exists => Trigger the request for previous entries. These changes also spotted an existing bug where the `hasMoreBefore` was never set to the API response value, but was always `true`, preventing also showing the "Expand time range" call to action on the scenario described above. ## 🧪 Testing - Navigate to Logs stream - Jump to a point in time that has no logs previously ingested - Verify the error is not logged when scrolling and that the "extend range" button is shown https://user-images.githubusercontent.com/34506779/230292210-c2690c37-efa1-4b05-a237-f14eb78d26eb.mov --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: Carlos Crespo --- .../containers/logs/log_stream/index.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index e1b425b112f8c..1e63c4f5204c7 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -109,7 +109,7 @@ export function useLogStream({ ...(resetOnSuccess ? INITIAL_STATE : prevState), entries: combined.entries, hasMoreAfter: combined.hasMoreAfter ?? prevState.hasMoreAfter, - hasMoreBefore: combined.hasMoreAfter ?? prevState.hasMoreAfter, + hasMoreBefore: combined.hasMoreBefore ?? prevState.hasMoreBefore, bottomCursor: combined.bottomCursor, topCursor: combined.topCursor, lastLoadedTime: new Date(), @@ -151,9 +151,9 @@ export function useLogStream({ const fetchPreviousEntries = useCallback( (params) => { - if (state.topCursor === null) { + if (state.topCursor === null && state.hasMoreBefore) { throw new Error( - 'useLogStream: Cannot fetch previous entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' + 'useLogStream: Cannot fetch previous entries.\nIt seems there are more entries available, but no cursor is set.\nEnsure you have called `fetchEntries` at least once.' ); } @@ -161,10 +161,12 @@ export function useLogStream({ return; } - fetchLogEntriesBefore(state.topCursor, { - size: LOG_ENTRIES_CHUNK_SIZE, - extendTo: params?.extendTo, - }); + if (state.topCursor !== null) { + fetchLogEntriesBefore(state.topCursor, { + size: LOG_ENTRIES_CHUNK_SIZE, + extendTo: params?.extendTo, + }); + } }, [fetchLogEntriesBefore, state.topCursor, state.hasMoreBefore] ); @@ -198,7 +200,7 @@ export function useLogStream({ const fetchNextEntries = useCallback( (params) => { - if (state.bottomCursor === null) { + if (state.bottomCursor === null && state.hasMoreAfter) { throw new Error( 'useLogStream: Cannot fetch next entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' ); @@ -208,10 +210,12 @@ export function useLogStream({ return; } - fetchLogEntriesAfter(state.bottomCursor, { - size: LOG_ENTRIES_CHUNK_SIZE, - extendTo: params?.extendTo, - }); + if (state.bottomCursor !== null) { + fetchLogEntriesAfter(state.bottomCursor, { + size: LOG_ENTRIES_CHUNK_SIZE, + extendTo: params?.extendTo, + }); + } }, [fetchLogEntriesAfter, state.bottomCursor, state.hasMoreAfter] ); From 8f4fc45a18b8b35b6eefac8ff3b54d2318b00b12 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Apr 2023 12:13:26 +0200 Subject: [PATCH 009/100] [Exp View] Fix e2e test (#155123) --- .../e2e/journeys/step_duration.journey.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/exploratory_view/e2e/journeys/step_duration.journey.ts b/x-pack/plugins/exploratory_view/e2e/journeys/step_duration.journey.ts index 706de17fcc661..a109740d74496 100644 --- a/x-pack/plugins/exploratory_view/e2e/journeys/step_duration.journey.ts +++ b/x-pack/plugins/exploratory_view/e2e/journeys/step_duration.journey.ts @@ -5,23 +5,16 @@ * 2.0. */ -import { journey, step, before, after } from '@elastic/synthetics'; +import { journey, step } from '@elastic/synthetics'; import moment from 'moment'; import { recordVideo } from '../record_video'; import { createExploratoryViewUrl } from '../../public/components/shared/exploratory_view/configurations/exploratory_view_url'; -import { loginToKibana, TIMEOUT_60_SEC, waitForLoadingToFinish } from '../utils'; +import { byTestId, loginToKibana, TIMEOUT_60_SEC, waitForLoadingToFinish } from '../utils'; -journey('Exploratory view', async ({ page, params }) => { +journey('Step Duration series', async ({ page, params }) => { recordVideo(page); - before(async () => { - await waitForLoadingToFinish({ page }); - }); - - after(async () => { - // eslint-disable-next-line no-console - console.log(await page.video()?.path()); - }); + page.setDefaultTimeout(TIMEOUT_60_SEC.timeout); const expUrl = createExploratoryViewUrl({ reportType: 'kpi-over-time', @@ -54,16 +47,16 @@ journey('Exploratory view', async ({ page, params }) => { }); }); - step('Open exploratory view with monitor duration', async () => { + step('build series with monitor duration', async () => { await page.waitForNavigation(TIMEOUT_60_SEC); await waitForLoadingToFinish({ page }); - await page.click('text=browser', TIMEOUT_60_SEC); + await page.click('text=browser'); await page.click('text=http'); await page.click('[aria-label="Remove report metric"]'); await page.click('button:has-text("Select report metric")'); await page.click('button:has-text("Step duration")'); - await page.click('text=Select an option: Monitor type, is selectedMonitor type >> button'); + await page.click(byTestId('seriesBreakdown')); await page.click('button[role="option"]:has-text("Step name")'); await page.click('.euiComboBox__inputWrap'); await page.click( @@ -71,7 +64,9 @@ journey('Exploratory view', async ({ page, params }) => { ); await page.click('button[role="option"]:has-text("test-monitor - inline")'); await page.click('button:has-text("Apply changes")'); + }); + step('Verify that changes are applied', async () => { await waitForLoadingToFinish({ page }); await page.click('[aria-label="series color: #54b399"]'); From 0ce4f2de8ac8585055a713f5d39e6f5b727ec9cf Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 18 Apr 2023 12:44:51 +0200 Subject: [PATCH 010/100] [Lens] Reenable reporting functional tests (#155118) ## Summary Fixes #155047, #154958, #154957, #154956, #154955, #154954 Flaky runner with 50 runs: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2146 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../functional/apps/lens/group3/lens_reporting.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/lens/group3/lens_reporting.ts b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts index bd7a27a411e7c..dc241c7f3ac49 100644 --- a/x-pack/test/functional/apps/lens/group3/lens_reporting.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts @@ -24,8 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const browser = getService('browser'); - // Failing: See https://github.com/elastic/kibana/issues/154958 - describe.skip('lens reporting', () => { + describe('lens reporting', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/lens/reporting' @@ -60,6 +59,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.clickGenerateReportButton(); const url = await PageObjects.reporting.getReportURL(60000); expect(url).to.be.ok(); + if (await testSubjects.exists('toastCloseButton')) { + await testSubjects.click('toastCloseButton'); + } }); for (const type of ['PNG', 'PDF'] as const) { @@ -101,6 +103,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.clickGenerateReportButton(); const url = await PageObjects.reporting.getReportURL(60000); expect(url).to.be.ok(); + if (await testSubjects.exists('toastCloseButton')) { + await testSubjects.click('toastCloseButton'); + } }); it(`should show a warning message for curl reporting of unsaved visualizations`, async () => { @@ -126,6 +131,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it(`should produce a valid URL for reporting`, async () => { await PageObjects.reporting.clickGenerateReportButton(); await PageObjects.reporting.getReportURL(60000); + if (await testSubjects.exists('toastCloseButton')) { + await testSubjects.click('toastCloseButton'); + } // navigate to the reporting page await PageObjects.common.navigateToUrl('management', '/insightsAndAlerting'); await testSubjects.click('reporting'); From 7de4733f7255c976bf6503669e31e939cb9f485f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 18 Apr 2023 13:06:05 +0200 Subject: [PATCH 011/100] [FullStory] Update snippet (#153570) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- NOTICE.txt | 5 +++-- packages/analytics/shippers/fullstory/src/load_snippet.ts | 2 +- .../cloud_full_story/server/assets/fullstory_library.js | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index cd0ba2a9c1bd9..45af6e5231783 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -257,8 +257,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- -This code is part of the Services provided by FullStory, Inc. For license information, please refer to https://www.fullstory.com/legal/terms-and-conditions/. -Portions of this code are licensed separately and can be found in https://edge.fullstory.com/s/fs.js.LICENSE.txt +This code is part of the Services provided by FullStory, Inc. For license information, please refer to https://www.fullstory.com/legal/terms-and-conditions/ +Portions of this code are licensed under the following license: + For license information please see fs.js.LICENSE.txt --- This product bundles bootstrap@3.3.6 which is available under a diff --git a/packages/analytics/shippers/fullstory/src/load_snippet.ts b/packages/analytics/shippers/fullstory/src/load_snippet.ts index 2c37eee6608e9..1cbcc96e201ef 100644 --- a/packages/analytics/shippers/fullstory/src/load_snippet.ts +++ b/packages/analytics/shippers/fullstory/src/load_snippet.ts @@ -36,7 +36,7 @@ export interface FullStorySnippetConfig { } export function loadSnippet({ - scriptUrl = 'edge.fullstory.com/s/fs.js', + scriptUrl = 'https://edge.fullstory.com/s/fs.js', fullStoryOrgId, host = 'fullstory.com', namespace = 'FS', diff --git a/x-pack/plugins/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js b/x-pack/plugins/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js index dcc579b5e57b9..bfe2af8d44668 100644 --- a/x-pack/plugins/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js +++ b/x-pack/plugins/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js @@ -1,8 +1,9 @@ /* @notice - * This code is part of the Services provided by FullStory, Inc. For license information, please refer to https://www.fullstory.com/legal/terms-and-conditions/. - * Portions of this code are licensed separately and can be found in https://edge.fullstory.com/s/fs.js.LICENSE.txt + * This code is part of the Services provided by FullStory, Inc. For license information, please refer to https://www.fullstory.com/legal/terms-and-conditions/ + * Portions of this code are licensed under the following license: + * For license information please see fs.js.LICENSE.txt */ /* eslint-disable prettier/prettier,no-var,eqeqeq,new-cap,no-nested-ternary,no-use-before-define,no-sequences,block-scoped-var,one-var, dot-notation,no-script-url,no-restricted-globals,no-unused-vars,guard-for-in,no-proto,camelcase,no-empty,no-redeclare,no-caller, strict,no-extend-native,no-undef,no-loop-func */ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/s",n(n.s=4)}([function(e,t,n){"use strict";var r=this&&this.__assign||function(){return(r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0},t.assertExhaustive=function(e,t){throw void 0===t&&(t="Reached unexpected case in exhaustive switch"),new Error(t)},t.pick=function(e){for(var t=[],n=1;na&&(i=a);var u=n.split(/[#,]/);if(u.length<3&&(u=n.split("`")).length<3)return null;var c=u[0],h=u[1],d=u[2],l=u[3],p="";void 0!==l&&(p=decodeURIComponent(l),(f.indexOf(p)>=0||v.indexOf(p)>=0)&&(o("Ignoring invalid app key \""+p+"\" from cookie."),p=""));var m=d.split(":");return{expirationAbsTimeSeconds:i,host:c,orgId:h,userId:m[0],sessionId:m[1]||"",appKeyHash:p}}function y(e){for(var t={},n=e.cookie.split(";"),r=0;r=0&&(t=t.slice(0,n)),t}(e))?e:0==e.indexOf("www.")?"app."+e.slice(4):"app."+e:e}function H(e){return e?e+"/s/fs.js":void 0}!function(e){e.MUT_INSERT=2,e.MUT_REMOVE=3,e.MUT_ATTR=4,e.MUT_TEXT=6,e.MOUSEMOVE=8,e.MOUSEMOVE_CURVE=9,e.SCROLL_LAYOUT=10,e.SCROLL_LAYOUT_CURVE=11,e.MOUSEDOWN=12,e.MOUSEUP=13,e.KEYDOWN=14,e.KEYUP=15,e.CLICK=16,e.FOCUS=17,e.VALUECHANGE=18,e.RESIZE_LAYOUT=19,e.DOMLOADED=20,e.LOAD=21,e.PLACEHOLDER_SIZE=22,e.UNLOAD=23,e.BLUR=24,e.SET_FRAME_BASE=25,e.TOUCHSTART=32,e.TOUCHEND=33,e.TOUCHCANCEL=34,e.TOUCHMOVE=35,e.TOUCHMOVE_CURVE=36,e.NAVIGATE=37,e.PLAY=38,e.PAUSE=39,e.RESIZE_VISUAL=40,e.RESIZE_VISUAL_CURVE=41,e.RESIZE_DOCUMENT=42,e.LOG=48,e.ERROR=49,e.DBL_CLICK=50,e.FORM_SUBMIT=51,e.WINDOW_FOCUS=52,e.WINDOW_BLUR=53,e.HEARTBEAT=54,e.WATCHED_ELEM=56,e.PERF_ENTRY=57,e.REC_FEAT_SUPPORTED=58,e.SELECT=59,e.CSSRULE_INSERT=60,e.CSSRULE_DELETE=61,e.FAIL_THROTTLED=62,e.AJAX_REQUEST=63,e.SCROLL_VISUAL_OFFSET=64,e.SCROLL_VISUAL_OFFSET_CURVE=65,e.MEDIA_QUERY_CHANGE=66,e.RESOURCE_TIMING_BUFFER_FULL=67,e.MUT_SHADOW=68,e.DISABLE_STYLESHEET=69,e.FULLSCREEN=70,e.FULLSCREEN_ERROR=71,e.ADOPTED_STYLESHEETS=72,e.CUSTOM_ELEMENT_DEFINED=73,e.MODAL_OPEN=74,e.MODAL_CLOSE=75,e.SLOW_INTERACTION=76,e.LONG_FRAME=77,e.TIMING=78,e.STORAGE_WRITE_FAILURE=79,e.KEEP_ELEMENT=2e3,e.KEEP_URL=2001,e.KEEP_BOUNCE=2002,e.SYS_SETVAR=8193,e.SYS_RESOURCEHASH=8195,e.SYS_SETCONSENT=8196,e.SYS_CUSTOM=8197,e.SYS_REPORTCONSENT=8198}(R||(R={})),function(e){e.Unknown=0,e.Serialization=1}(A||(A={})),function(e){e.Unknown=0,e.DomSnapshot=1,e.NodeEncoding=2,e.LzEncoding=3}(x||(x={})),function(e){e.Internal=0,e.Public=1}(O||(O={}));var j,K,V,z,Y,G,Q,X,J,$,Z,ee,te,ne=["print","alert","confirm"];function re(e){switch(e){case R.MOUSEDOWN:case R.MOUSEMOVE:case R.MOUSEMOVE_CURVE:case R.MOUSEUP:case R.KEYDOWN:case R.KEYUP:case R.TOUCHSTART:case R.TOUCHEND:case R.TOUCHMOVE:case R.TOUCHMOVE_CURVE:case R.TOUCHCANCEL:case R.CLICK:case R.SCROLL_LAYOUT:case R.SCROLL_LAYOUT_CURVE:case R.SCROLL_VISUAL_OFFSET:case R.SCROLL_VISUAL_OFFSET_CURVE:case R.NAVIGATE:return!0;}return!1}!function(e){e.GrantConsent=!0,e.RevokeConsent=!1}(j||(j={})),function(e){e.Page=0,e.Document=1}(K||(K={})),function(e){e.Unknown=0,e.Api=1,e.FsShutdownFrame=2,e.Hibernation=3,e.Reidentify=4,e.SettingsBlocked=5,e.Size=6,e.Unload=7}(V||(V={})),function(e){e.Timing=0,e.Navigation=1,e.Resource=2,e.Paint=3,e.Mark=4,e.Measure=5,e.Memory=6}(z||(z={})),function(e){e.Performance=0,e.PerformanceEntries=1,e.PerformanceMemory=2,e.Console=3,e.Ajax=4,e.PerformanceObserver=5,e.AjaxFetch=6}(Y||(Y={})),function(e){e.Node=1,e.Sheet=2}(G||(G={})),function(e){e.StyleSheetHooks=0,e.SetPropertyHooks=1}(Q||(Q={})),function(e){e.User="user",e.Account="acct",e.Event="evt"}(X||(X={})),function(e){e.Elide=0,e.Record=1,e.Whitelist=2}(J||(J={})),function(e){e.ReasonNoSuchOrg=1,e.ReasonOrgDisabled=2,e.ReasonOrgOverQuota=3,e.ReasonBlockedDomain=4,e.ReasonBlockedIp=5,e.ReasonBlockedUserAgent=6,e.ReasonBlockedGeo=7,e.ReasonBlockedTrafficRamping=8,e.ReasonInvalidURL=9,e.ReasonUserOptOut=10,e.ReasonInvalidRecScript=11,e.ReasonDeletingUser=12,e.ReasonNativeHookFailure=13}($||($={})),function(e){e.Unset=0,e.Exclude=1,e.Mask=2,e.Unmask=3,e.Watch=4,e.Keep=5}(Z||(Z={})),function(e){e.Unset=0,e.Click=1}(ee||(ee={})),function(e){e.MaxLogsPerPage=1024,e.MutationProcessingInterval=250,e.CurveSamplingInterval=142,e.DefaultBundleUploadInterval=5e3,e.HeartbeatInitial=4e3,e.HeartbeatMax=256200,e.PageInactivityTimeout=18e5,e.BackoffMax=3e5,e.ScrollSampleInterval=e.MutationProcessingInterval/5,e.InactivityThreshold=4e3,e.MaxPayloadLength=16384}(te||(te={}));function ie(e,t){return function(){try{return e.apply(this,arguments)}catch(e){try{t&&t(e)}catch(e){}}}}var oe=function(){},se=navigator.userAgent,ae=se.indexOf("MSIE ")>-1||se.indexOf("Trident/")>-1,ue=(ae&&se.indexOf("Trident/5"),ae&&se.indexOf("Trident/6"),ae&&se.indexOf("rv:11")>-1),ce=se.indexOf("Edge/")>-1;se.indexOf("CriOS");var he=/^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);function de(){var e=window.navigator.userAgent.match(/Version\/(\d+)/);return e?parseInt(e[1]):-1}function le(e){if(!he)return!1;var t=de();return t>=0&&t===e}function pe(e){if(!he)return!1;var t=de();return t>=0&&tt)return!1;return n==t}function pt(e,t){var n=0;for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)&&++n>t)return!0;return!1}Ze="function"==typeof s.objectKeys?function(e){return s.objectKeys(e)}:function(e){var t=[];for(var n in e)s.objectHasOwnProp(e,n)&&t.push(n);return t},st=(ot=function(e){return e.matches||e.msMatchesSelector||e.webkitMatchesSelector})(window.Element.prototype),!(at=window.document?window.document.documentElement:void 0)||st&&at instanceof window.Element||(st=ot(at)),it=($e=[st,function(e,t){return st.call(e,t)}])[0],rt=$e[1];var ft;ut=ae?function(e){var t=e.nextSibling;return t&&e.parentNode&&t===e.parentNode.firstChild?null:t}:function(e){return e.nextSibling};var vt;ft=ae?function(e,t){if(e){var n=e.parentNode?e.parentNode.firstChild:null;do{t(e),e=e.nextSibling}while(e&&e!=n)}}:function(e,t){for(;e;e=e.nextSibling)t(e)};vt=ae?function(e){var t=e.previousSibling;return t&&e.parentNode&&t===e.parentNode.lastChild?null:t}:function(e){return e.previousSibling};function _t(e,t){if(!e)return oe;var n=function(e){try{var t=window;return t.Zone&&t.Zone.root&&"function"==typeof t.Zone.root.wrap?t.Zone.root.wrap(e):e}catch(t){return e}}(e);return t&&(n=n.bind(t)),ie(n,function(e){o("Unexpected error: "+e)})}function gt(e){var t,n=Array.prototype.toJSON,r=String.prototype.toJSON;n&&(Array.prototype.toJSON=void 0),r&&(String.prototype.toJSON=void 0);try{t=s.jsonStringify(e)}catch(e){t=mt(e)}finally{n&&(Array.prototype.toJSON=n),r&&(String.prototype.toJSON=r)}return t}function mt(e){var t="Internal error: unable to determine what JSON error was";try{t=(t=""+e).replace(/[^a-zA-Z0-9\.\:\!\, ]/g,"_")}catch(e){}return"\""+t+"\""}function yt(e){var t=e.doctype;if(!t)return"";var n=""}function wt(e){return s.jsonParse(e)}function bt(e){var t=0,n=0;return null==e.screen?[t,n]:(t=parseInt(String(e.screen.width)),n=parseInt(String(e.screen.height)),[t=isNaN(t)?0:t,n=isNaN(n)?0:n])}var St=function(){function e(e,t){this.target=e,this.propertyName=t,this._before=oe,this._afterSync=oe,this._afterAsync=oe,this.on=!1}return e.prototype.before=function(e){return this._before=_t(e),this},e.prototype.afterSync=function(e){return this._afterSync=_t(e),this},e.prototype.afterAsync=function(e){return this._afterAsync=_t(function(t){s.setWindowTimeout(window,ie(function(){e(t)}),0)}),this},e.prototype.disable=function(){if(this.on=!1,this.shim){var e=this.shim,t=e.override,n=e["native"];this.target[this.propertyName]===t&&(this.target[this.propertyName]=n,this.shim=void 0)}},e.prototype.enable=function(){if(this.on=!0,this.shim)return!0;this.shim=this.makeShim();try{this.target[this.propertyName]=this.shim.override}catch(e){return!1}return!0},e.prototype.makeShim=function(){var e=this,t=this.target[this.propertyName];return{"native":t,override:function(){var n={that:this,args:arguments,result:null};e.on&&e._before(n);var r=t.apply(this,arguments);return e.on&&(n.result=r,e._afterSync(n),e._afterAsync(n)),r}}},e}(),Et={};function Tt(e,t){if(!e||"function"!=typeof e[t])return null;var n;Et[t]=Et[t]||[];for(var r=0;r\n";var n=[];try{for(var r=arguments.callee.caller.caller;r&&n.lengthn)return!1;var r=new Error("Assertion failed: "+t);return Mt.sendToBugsnag(r,"error"),e}function Pt(e,t,n,r){void 0!==n&&("function"==typeof e.addEventListener?e.addEventListener(t,n,r):"function"==typeof e.addListener?e.addListener(n):o("Target of "+t+" doesn't seem to support listeners"))}function qt(e,t,n,r){void 0!==n&&("function"==typeof e.removeEventListener?e.removeEventListener(t,n,r):"function"==typeof e.removeListener?e.removeListener(n):o("Target of "+t+" doesn't seem to support listeners"))}var Ut=function(){function e(){var e=this;this._listeners=[],this._children=[],this._yesCapture=!0,this._noCapture=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e._yesCapture={capture:!0,passive:!0},e._noCapture={capture:!1,passive:!0}}});window.addEventListener("test",oe,t)}catch(e){}}return e.prototype.add=function(e,t,n,r,i){return void 0===i&&(i=!1),this.addCustom(e,t,n,r,i)},e.prototype.addCustom=function(e,t,n,r,i){void 0===i&&(i=!1);var o={target:e,type:t,fn:Mt.wrap(function(e){(i||!1!==e.isTrusted||"message"==t||e._fs_trust_event)&&r(e)}),options:n?this._yesCapture:this._noCapture,index:this._listeners.length};return this._listeners.push(o),Pt(e,t,o.fn,o.options),o},e.prototype.remove=function(e){e.target&&(qt(e.target,e.type,e.fn,e.options),e.target=null,e.fn=void 0)},e.prototype.clear=function(){for(var e=0;e0&&t.height>0)return this.width=t.width,void(this.height=t.height);r=this.computeLayoutViewportSizeFromMediaQueries(e),this.width=r[0],this.height=r[1]}}return e.prototype.computeLayoutViewportSizeFromMediaQueries=function(e){var t=this.findMediaValue(e,"width",this.clientWidth,this.clientWidth+128);void 0===t&&(t=this.tryToGet(e,"innerWidth")),void 0===t&&(t=this.clientWidth);var n=this.findMediaValue(e,"height",this.clientHeight,this.clientHeight+128);return void 0===n&&(n=this.tryToGet(e,"innerHeight")),void 0===n&&(n=this.clientHeight),[t,n]},e.prototype.findMediaValue=function(e,t,n,r){if(s.matchMedia){var i=s.matchMedia(e,"(min-"+t+": "+n+"px)");if(null!=i){if(i.matches&&s.matchMedia(e,"(max-"+t+": "+n+"px)").matches)return n;for(;n<=r;){var o=s.mathFloor((n+r)/2);if(s.matchMedia(e,"(min-"+t+": "+o+"px)").matches){if(s.matchMedia(e,"(max-"+t+": "+o+"px)").matches)return o;n=o+1}else r=o-1}}}},e.prototype.tryToGet=function(e,t){try{return e[t]}catch(e){return}},e}();function Yt(e,t){return new zt(e,t)}var Gt=function(e,t){this.offsetLeft=0,this.offsetTop=0,this.pageLeft=0,this.pageTop=0,this.width=0,this.height=0,this.scale=0;var n=e.document;if(n.body){var r="BackCompat"==n.compatMode;"pageXOffset"in e?(this.pageLeft=e.pageXOffset,this.pageTop=e.pageYOffset):n.scrollingElement?(this.pageLeft=n.scrollingElement.scrollLeft,this.pageTop=n.scrollingElement.scrollTop):r?(this.pageLeft=n.body.scrollLeft,this.pageTop=n.body.scrollTop):n.documentElement&&(n.documentElement.scrollLeft>0||n.documentElement.scrollTop>0)?(this.pageLeft=n.documentElement.scrollLeft,this.pageTop=n.documentElement.scrollTop):(this.pageLeft=n.body.scrollLeft||0,this.pageTop=n.body.scrollTop||0),this.offsetLeft=this.pageLeft-t.pageLeft,this.offsetTop=this.pageTop-t.pageTop;try{var i=e.innerWidth,o=e.innerHeight}catch(e){return}if(0!=i&&0!=o){this.scale=t.width/i,this.scale<1&&(this.scale=1);var s=t.width-t.clientWidth,a=t.height-t.clientHeight;this.width=i-s/this.scale,this.height=o-a/this.scale}}};var Qt,Xt=(Qt=function(e,t){return(Qt=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}Qt(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),Jt=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},$t=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]this._due)return et.resolve().then(this._wrappedTick)["catch"](function(){})},e.registry={},e.nextId=0,e.checkedAlready=!1,e.lastCheck=0,e}(),en=function(e){function t(t){var n=e.call(this)||this;return n._interval=t,n._handle=-1,n}return Xt(t,e),t.prototype.start=function(e){var t=this;-1==this._handle&&(this.setTick(function(){e(),t.register(t._interval)}),this._handle=s.setWindowInterval(window,this._wrappedTick,this._interval),this.register(this._interval))},t.prototype.cancel=function(){-1!=this._handle&&(s.clearWindowInterval(window,this._handle),this._handle=-1,this.setTick(function(){}))},t}(Zt),tn=function(e){function t(t,n,r){void 0===n&&(n=0);for(var i=[],o=3;ot&&(this._skew=e-t,this._reportTimeSkew("timekeeper set with future ts"))},e.prototype._reportTimeSkew=function(e){this._reported++<=2&&Mt.sendToBugsnag(e,"error",{skew:this._skew,startTime:this._startTime,wallTime:this.wallTime()})},e}();function on(e){var t=e;return t.tagName?"object"==typeof t.tagName?"form":t.tagName.toLowerCase():null}var sn,an,un=n(3),cn=n(0),hn=Object.defineProperty,dn=p()%1e9,ln=window.WeakMap||((sn=function(){this.name="__st"+(1e9*s.mathRandom()>>>0)+dn++ +"__"}).prototype={set:function(e,t){var n=e[this.name];n&&n[0]===e?n[1]=t:hn(e,this.name,{value:[e,t],writable:!0})},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0}},sn),pn=1,fn=4,vn=function(){for(var e=0,t=0,n=arguments.length;t0&&o(n[u],this._rules[u]),r[u].length>0&&o(r[u],this._consentRules[u])}},e.prototype._fallback=function(e){for(var t=0,n=e;t0&&t.length<1e4;){var n=t.pop();delete Tn[n.id],n.node._fs==n.id&&(n.node._fs=0),n.id=0,n.next&&t.push(n.next),n.child&&t.push(n.child)}Ft(t.length<1e4,"clearIds is fast")}var qn,Un=function(){function e(e,t){this._onchange=e,this._checkElem=t,this._fallback=!1,this._elems={},this.values={},this.radios={},qn=this}return e.prototype.hookEvents=function(){(function(){var e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");if(!e||!e.set)return!1;Nn||(kt(HTMLInputElement,"value",jn),kt(HTMLInputElement,"checked",jn),kt(HTMLSelectElement,"value",jn),kt(HTMLTextAreaElement,"value",jn),kt(HTMLSelectElement,"selectedIndex",jn),kt(HTMLOptionElement,"selected",jn),Nn=!0);return!0})()||(this._fallback=!0)},e.prototype.addInput=function(e){var t=Mn(e);if(this._elems[t]=e,Vn(e)){var n=Hn(e);e.checked&&(this.radios[n]=t)}else this.values[t]=Kn(e);(function(e){switch(e.type){case"checkbox":case"radio":return e.checked!=e.hasAttribute("checked");default:return(e.value||"")!=function(e){if("select"!=on(e))return e.getAttribute("value")||"";var t=e,n=t.querySelector("option[selected]")||t.querySelector("option");if(!n)return"";return n.value||""}(e);}})(e)&&this._onchange(e)},e.prototype.diffValue=function(e,t){var n=Mn(e);if(Vn(e)){var r=Hn(e);return this.radios[r]==n!=("true"==t)}return this.values[n]!=t},e.prototype.onChange=function(e,t){void 0===t&&(t=Kn(e));var n=Mn(e);if((e=this._elems[n])&&this.diffValue(e,t))if(this._onchange(e),Vn(e)){var r=Hn(e);"false"==t&&this.radios[r]==n?delete this.radios[r]:this.radios[r]=n}else this.values[n]=t},e.prototype.tick=function(){for(var e in this._elems){var t=this._elems[e];if(this._checkElem(t)){if(this._fallback){var n=Kn(t);if(t&&this.diffValue(t,n))if(this._onchange(t),Vn(t)){var r=Hn(t);this.radios[r]=+e}else this.values[e]=n}}else delete this._elems[e],delete this.values[e],Vn(t)&&delete this.radios[Hn(t)]}},e.prototype.shutdown=function(){qn=null},e.prototype._usingFallback=function(){return this._fallback},e.prototype._trackingElem=function(e){return!!this._elems[e]},e}(),Nn=!1;var Wn,Dn={};function Bn(){try{if(qn)for(var e in Dn){var t=Dn[e],n=t[0],r=t[1];qn.onChange(n,r)}}finally{Wn=null,Dn={}}}function Hn(e){if(!e)return"";for(var t=e;t&&"form"!=on(t);)t=t.parentElement;return(t&&"form"==on(t)?Mn(t):0)+":"+e.name}function jn(e,t){var n=function e(t,n){if(void 0===n&&(n=2),n<=0)return t;var r=on(t);return"option"!=r&&"optgroup"!=r||!t.parentElement?t:e(t.parentElement,n-1)}(e),r=Mn(n);r&&qn&&qn.diffValue(n,""+t)&&(Dn[r]=[n,""+t],Wn||(Wn=new tn(Bn)).start())}function Kn(e){switch(e.type){case"checkbox":case"radio":return""+e.checked;default:var t=e.value;return t||(t=""),""+t;}}function Vn(e){return e&&"radio"==e.type}var zn={};var Yn="__default";function Gn(e){void 0===e&&(e=Yn);var t=zn[e];return t||(t=function(){var e=document.implementation.createHTMLDocument("");return e.head||e.documentElement.appendChild(e.createElement("head")),e.body||e.documentElement.appendChild(e.createElement("body")),e}(),e!==Yn&&(t.open(),t.write(e),t.close()),zn[e]=t),t}var Qn=new(function(){function e(){var e=Gn(),t=e.getElementById("urlresolver-base");t||((t=e.createElement("base")).id="urlresolver-base",e.head.appendChild(t));var n=e.getElementById("urlresolver-parser");n||((n=e.createElement("a")).id="urlresolver-parser",e.head.appendChild(n)),this.base=t,this.parser=n}return e.prototype.parseUrl=function(e,t){if("undefined"!=typeof URL)try{e||(e=document.baseURI);var n=e?new URL(t,e):new URL(t);if(n.href)return n}catch(e){}return this.parseUrlUsingBaseAndAnchor(e,t)},e.prototype.parseUrlUsingBaseAndAnchor=function(e,t){this.base.setAttribute("href",e),this.parser.setAttribute("href",t);var n=document.createElement("a");return n.href=this.parser.href,n},e.prototype.resolveUrl=function(e,t){return this.parseUrl(e,t).href},e.prototype.resolveToDocument=function(e,t){var n=Jn(e);return null==n?t:this.resolveUrl(n,t)},e}());function Xn(e,t){return Qn.parseUrl(e,t)}function Jn(e){var t=e.document,n=e.location.href;if("string"==typeof t.baseURI)n=t.baseURI;else{var r=t.getElementsByTagName("base")[0];r&&r.href&&(n=r.href)}return"about:blank"==n&&e.parent!=e?Jn(e.parent):n}var $n=new RegExp("[^\\s]"),Zn=new RegExp("[\\s]*$");String.prototype;function er(e){var t=$n.exec(e);if(!t)return e;for(var n=t.index,r=(t=Zn.exec(e))?e.length-t.index:0,i="\uFFFF",o=e.slice(n,e.length-r).split(/\r\n?|\n/g),s=0;sir?(Mt.sendToBugsnag("Ignoring huge text node","warning",{length:r}),""):e.parentNode&&"style"==on(e.parentNode)?n:t.mask?er(n):n}function sr(e){return tr[e]||e.toLowerCase()}function ar(e,t,n,r){var i,o=on(t);if(null===o)return null;var s=function(e){var t,r,s;i=null!==(r=null===(t=nr[e][o])||void 0===t?void 0:t[n])&&void 0!==r?r:null===(s=nr[e]["*"])||void 0===s?void 0:s[n]};if(s("Any"),void 0===i){var a=xn(t);if(!a)return null;a.watchKind==an.Exclude?s("Exclude"):a.mask&&s("Mask")}if(void 0===i)return r;switch(i){case"erase":return null;case"scrubUrl":return ur(r,e,{source:"dom",type:o});case"maskText":return er(r);default:return Object(cn.assertExhaustive)(i);}}function ur(e,t,n){switch(n.source){case"dom":switch(r=n.type){case"frame":case"iframe":return hr(e,t);default:return cr(e,t);}case"event":switch(r=n.type){case R.AJAX_REQUEST:case R.NAVIGATE:return cr(e,t);case R.SET_FRAME_BASE:return hr(e,t);default:return Object(cn.assertExhaustive)(r);}case"log":return hr(e,t);case"page":var r;switch(r=n.type){case"base":return hr(e,t);case"referrer":case"url":return cr(e,t);default:return Object(cn.assertExhaustive)(r);}case"perfEntry":switch(n.type){case"frame":case"iframe":case"navigation":case"other":return hr(e,t);default:return cr(e,t);}default:return Object(cn.assertExhaustive)(n);}}function cr(e,t){return lr(e,t,function(e){if(!(e in pr)){var t=["password","token","^jwt$"];switch("4K3FQ"!==e&&"NQ829"!==e&&"KCF98"!==e&&t.push("^code$"),e){case"2FVM4":t.push("^e$","^eref$","^fn$");break;case"35500":t.push("share_token","password-reset-key");break;case"1HWDJ":t.push("email_id","invite","join");break;case"J82WF":t=[".*"];break;case"8MM83":t=["^creditCard"];break;case"PAN8Z":t.push("code","hash","ol","aeh");break;case"BKP05":t.push("api_key","session_id","encryption_key");break;case"QKM7G":t.push("postcode","encryptedQuoteId","registrationId","productNumber","customerName","agentId","qqQuoteId");break;case"FP60X":t.push("phrase");break;case"GDWG7":t=["^(?!productType|utmSource).*$"];break;case"RV68C":t.push("drivingLicense");break;case"S3VEC":t.push("data");break;case"Q8RZE":t.push("myLowesCardNumber");}pr[e]=new RegExp(t.join("|"),"i")}return pr[e]}(t))}function hr(e,t){return lr(e,t,fr)}function dr(e,t,n,r){var i=new RegExp("(\\/"+t+"\\/).*$","i");n==r&&e.pathname.indexOf(t)>=0&&(e.pathname=e.pathname.replace(i,"$1"+rr))}function lr(e,t,n){var r=Xn("",e);return r.hash&&r.hash.indexOf("access_token")>=0&&(r.hash="#"+rr),dr(r,"visitor",t,"QS8RG"),dr(r,"account",t,"QS8RG"),dr(r,"parentAccount",t,"QS8RG"),dr(r,"reset_password",t,"AGQFM"),dr(r,"reset-password",t,"95NJ7"),dr(r,"dl",t,"RV68C"),dr(r,"retailer",t,"FP60X"),dr(r,"ocadotech",t,"FP60X"),dr(r,"serviceAccounts",t,"FP60X"),dr(r,"signup",t,"7R98D"),r.search&&r.search.length>0&&(r.search=function(e,t){return e.split("?").map(function(e){return function(e,t){return e.replace("?","").split("&").map(function(e){return e.split("=")}).map(function(e){var n=e[0],r=e[1],i=e.slice(2);return t.test(n)&&void 0!==r?[n,rr].concat(i):[n,r].concat(i)}).map(function(e){var t=e[0],n=e[1],r=e.slice(2);return void 0===n?t:[t,n].concat(r).join("=")}).join("&")}(e,t)}).join("?")}(r.search,n)),r.href.substring(0,2048)}var pr={};var fr=new RegExp(".*","i");var vr=/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/gi,_r=/(?:(http)|(ftp)|(file))[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/gi;function gr(e){return"function"==typeof(t=e.constructor)&&Function.prototype.toString.call(t).indexOf("[native code]")>-1;var t}var mr=function(){function e(e,t,n){this._watcher=e,this._resizer=t,this._orgId=n,Tn={},kn=1}return e.prototype.tokenizeNode=function(e,t,n,r,i,o,s){var a=this,u=xn(t),c=xn(n),h=[];return function(e){var t=kn;try{return e(),!0}catch(e){return kn=t,!1}}(function(){a.tokeNode(e,u,c,r,h,i,o,s)})||(h=[]),h},e.prototype.tokeNode=function(e,t,n,r,i,o,s,a){for(var u=[{parentMirror:t,nextMirror:n,node:r}],c=function(){var t=u.pop();if(!t)return"continue";if("string"==typeof t)return i.push(t),"continue";var n=t.parentMirror,r=t.nextMirror,c=t.node,d=h._encodeTagAndAttributes(e,n,r,c,i,o,s);if(null==d||d.watchKind===an.Exclude)return"continue";var l=c.nodeType===pn?c.shadowRoot:null;return(l||c.firstChild)&&a(d)?(u.push("]"),function(e,t){if(!e)return;var n=[];ft(e,function(e){return n.push(e)});for(;n.length>0;){var r=n.pop();r&&t(r)}}(c.firstChild,function(e){u.push({parentMirror:d,nextMirror:null,node:e})}),l&&u.push({parentMirror:d,nextMirror:null,node:l}),void u.push("[")):"continue"},h=this;u.length;)c()},e.prototype._encodeTagAndAttributes=function(e,t,n,r,i,o,s){if("script"==on(r)||8==r.nodeType)return null;var a,u,c,h,d=function(e){return e.constructor===window.ShadowRoot}(r),l=function(e){var t={id:kn++,node:e};return Tn[t.id]=t,e._fs=t.id,t}(r);if((d||(null==t?void 0:t.isInShadowDOM))&&(l.isInShadowDOM=!0),t&&(d?t.shadow=l:(a=t,u=l,c=n,h=this._resizer,Fn(u,h),u.parent=a,u.next=c,c&&(u.prev=c.prev,c.prev=u),null==u.next?(u.prev=a.lastChild,a.lastChild=u):u.next.prev=u,null==u.prev?a.child=u:u.prev.next=u)),10==r.nodeType){var p=r;return i.push("1?s.push([o.idx,a]):s.push(o.idx):s.push(i)}for(n=1;n1e3)return null;if(!n||n.nodeType!=pn)return null;var r=n;if(getComputedStyle(r).display.indexOf("inline")<0)return r;n=n.parentNode}},t}(Er),kr=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return br(t,e),t.prototype.observe=function(e){var t=this;if(e&&e.nodeType==pn){var n=e;this.growWatchedIndex(xn(e)),this._ctx.measurer.requestMeasureTask(function(){t.addEntry(n)})}},t.prototype.unobserveSubtree=function(e){var t=xn(e);t&&this.clearWatchedIndex(t)},t.prototype.nodeChanged=function(e){var t=this,n=this.relatedWatched(e);this._ctx.measurer.requestMeasureTask(function(){for(var e=0,r=n;e0){var r=zr(t[n-1],e);if(r)return void(t[n-1]=r)}else!function(e){qr.push(e),Pr||(Pr=!0,Mr(Ur))}(this.observer);t[n]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.addEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this.handleEventBound,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this.handleEventBound,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=Or.get(e);t||Or.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=Or.get(e),n=0;n0||this._toRefresh.length>0){var n={},r={};for(var i in this.processRecords(e,t,r,n),r){var o=i.split("\t");t.push({Kind:R.MUT_ATTR,When:e,Args:[parseInt(o[0]),o[1],r[i]]})}for(var i in n)t.push({Kind:R.MUT_TEXT,When:e,Args:[parseInt(i),n[i]]})}var s=this._newShadowContainers;this._newShadowContainers=[];for(var a=0;a0)for(var d=0;d0){s[h]=c.target;var p=Jr(c.target);p&&(s[p.id]=p.node)}break;case"characterData":Cn(c.target)||c.oldValue!=c.target.textContent&&(r[h]=or(c.target));break;case"attributes":var f=An(w=c.target);if(_n(this._watcher.isWatched(w))>_n(f)){a(w);break}var v=Xr(c.attributeNamespace)+(c.attributeName||""),_=sr(v);if(w.hasAttribute(v)){var g=c.target.getAttribute(v);c.oldValue!=g&&(g=ar(this._ctx.options.orgId,c.target,_,g||""),this._attrVisitor(c.target,_,g||""),null!==g&&(n[h+"\t"+_]=g))}else n[h+"\t"+_]=null;}}catch(e){}for(var m=0,y=this._toRefresh;m0&&n.push({When:t,Kind:R.MUT_SHADOW,Args:[o,this._compress?this._lz.encode(s):s]})},e.prototype.genInsert=function(e,t,n,r,i,o){var s=Mn(r)||-1,a=Mn(o)||-1,u=-1===s&&-1===a,c=p(),h=this.genDocStream(e,r,i,o),d=p()-c;if(h.length>0){var l=p(),f=this._compress?this._lz.encode(h):h,v=p()-l;n.push({When:t,Kind:R.MUT_INSERT,Args:[s,a,f]},{When:t,Kind:R.TIMING,Args:[[O.Internal,A.Serialization,u?x.DomSnapshot:x.NodeEncoding,t,d,[x.LzEncoding,v]]]})}},e.prototype.genDocStream=function(e,t,n,r){var i=this;if(t&&Cn(t))return[];for(var o=[],s=this._encoder.tokenizeNode(e,t,r,n,function(e){if(e.nodeType==pn){var t=e;t.shadowRoot&&i.observe(t.shadowRoot)}i._nodeVisitor(e,o)},this._attrVisitor,this._visitChildren),a=0,u=o;a0){var i=t[t.length-1];if(i.Kind==R.MUT_REMOVE)return void i.Args.push(r)}t.push({When:e,Kind:R.MUT_REMOVE,Args:[r]})},e.prototype.setUpIEWorkarounds=function(){var t=this;if(ue){var n=Object.getOwnPropertyDescriptor(Node.prototype,"textContent"),r=n&&n.set;if(!n||!r)throw new Error("Missing textContent setter -- not safe to record mutations.");Object.defineProperty(Element.prototype,"textContent",Gr(Gr({},n),{set:function(e){try{for(var t=void 0;t=this.firstChild;)this.removeChild(t);if(null===e||""==e)return;var n=(this.ownerDocument||document).createTextNode(e);this.appendChild(n)}catch(t){r&&r.call(this,e)}}}))}this._setPropertyThrottle=new nn(e.ThrottleMax,e.ThrottleInterval,function(){return new tn(function(){t._setPropertyWasThrottled=!0,t.tearDownIEWorkarounds()}).start()});var i=this._setPropertyThrottle.guard(function(e){e.cssText=e.cssText});this._setPropertyThrottle.open(),this._setPropertyHook=Tt(CSSStyleDeclaration.prototype,"setProperty"),this._setPropertyHook&&this._setPropertyHook.afterSync(function(e){i(e.that)}),this._removePropertyHook=Tt(CSSStyleDeclaration.prototype,"removeProperty"),this._removePropertyHook&&this._removePropertyHook.afterSync(function(e){i(e.that)})},e.prototype.tearDownIEWorkarounds=function(){this._setPropertyThrottle&&this._setPropertyThrottle.close(),this._setPropertyHook&&this._setPropertyHook.disable(),this._removePropertyHook&&this._removePropertyHook.disable()},e.prototype.updateConsent=function(){var e=this,t=xn(this._root);t&&function(e,t){for(var n=[e];n.length;){var r=n.pop();if(r){t(r);for(var i=r.child,o=r.shadow;i;)n.push(i),i=i.next;o&&n.push(o)}}}(t,function(t){var n=t.node;t.matchesAnyConsentRule&&e.refreshElement(n)})},e.prototype.refreshElement=function(e){Mn(e)&&this._toRefresh.push(e)},e.ThrottleMax=1024,e.ThrottleInterval=1e4,e}();function Xr(e){return void 0===e&&(e=""),null===e?"":{"http://www.w3.org/1999/xlink":"xlink:","http://www.w3.org/XML/1998/namespace":"xml:","http://www.w3.org/2000/xmlns/":"xmlns:"}[e]||""}function Jr(e){return!(null==e?void 0:e.shadowRoot)||gr(e.shadowRoot)?null:xn(e.shadowRoot)}var $r=["navigationStart","unloadEventStart","unloadEventEnd","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","domLoading","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd"],Zr=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","unloadEventStart","unloadEventEnd","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd","type","redirectCount","decodedBodySize","encodedBodySize","transferSize"],ei=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","decodedBodySize","encodedBodySize","transferSize"],ti=["name","startTime","duration"],ni=["jsHeapSizeLimit","totalJSHeapSize","usedJSHeapSize"],ri=function(){function e(e,t,n){this._ctx=e,this._queue=t,this._perfSupported=!1,this._timingSupported=!1,this._getEntriesSupported=!1,this._memorySupported=!1,this._lastUsedJSHeapSize=0,this._gotLoad=!1,this._observer=null,this._observedBatches=[];var r=window.performance;r&&(this._perfSupported=!0,r.timing&&(this._timingSupported=!0),r.memory&&(this._memorySupported=!0),"function"==typeof r.getEntries&&(this._getEntriesSupported=!0),this._listeners=n.createChild())}return e.prototype.start=function(e){var t=this;this._resourceUploader=e;var n=window.performance;n&&(this._ctx.recording.inFrame||this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Performance,this._timingSupported,Y.PerformanceEntries,this._getEntriesSupported,Y.PerformanceMemory,this._memorySupported,Y.PerformanceObserver,!!window.PerformanceObserver]}),this.observe(),!this._observer&&n.addEventListener&&n.removeEventListener&&this._listeners.add(n,"resourcetimingbufferfull",!0,function(){t._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})}),this.checkMemory())},e.prototype.onLoad=function(){this._gotLoad||(this._gotLoad=!0,this._timingSupported&&(this.recordTiming(performance.timing),this.checkForNewEntries()))},e.prototype.tick=function(e){this.checkMemory(),e&&this.checkForNewEntries()},e.prototype.shutdown=function(){this._listeners&&this._listeners.clear(),this._resourceUploader=void 0;var e=[];this._observer?(this._observer.takeRecords&&(e=this._observer.takeRecords()),this._observer.disconnect()):window.performance&&window.performance.getEntries&&(e=window.performance.getEntries()),e.length>300&&(e=e.slice(0,300),this._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})),this._observedBatches.push(e),this.tick(!0)},e.prototype.observe=function(){var e=this;if(!this._observer&&this._getEntriesSupported&&window.PerformanceObserver){this._observedBatches.push(performance.getEntries()),this._observer=new window.PerformanceObserver(function(t){var n=t.getEntries();e._observedBatches.push(n)});var t=["navigation","resource","measure","mark"];window.PerformancePaintTiming&&t.push("paint"),this._observer.observe({entryTypes:t})}},e.prototype.checkMemory=function(){if(this._memorySupported&&!this._ctx.recording.inFrame){var e=performance.memory;if(e){var t=e.usedJSHeapSize-this._lastUsedJSHeapSize;(0==this._lastUsedJSHeapSize||s.mathAbs(t/this._lastUsedJSHeapSize)>.2)&&(this.addPerfEvent(z.Memory,e,ni),this._lastUsedJSHeapSize=e.usedJSHeapSize)}}},e.prototype.recordEntry=function(e){switch(e.entryType){case"navigation":this.recordNavigation(e);break;case"resource":this.recordResource(e);break;case"paint":this.recordPaint(e);break;case"measure":this.recordMeasure(e);break;case"mark":this.recordMark(e);}},e.prototype.checkForNewEntries=function(){if(this._perfSupported&&this._getEntriesSupported){var e=this._observedBatches;this._observedBatches=[];for(var t=0,n=e;t=t&&(n=void 0),o[o.length-1]--,n&&n!==si&&r&&(o.push(s.objectKeys(n).length),a.push(u));o[o.length-1]<=0;)o.pop(),a.pop();return n})}catch(e){}return"[error serializing "+e.constructor.name+"]"}}var ui=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._hook&&(this._hook.disable(),this._hook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.fetch;(i||e.fetch)&&(this._hook=Tt(i?r._w:e,"fetch"),this._hook&&this._hook.afterSync(function(e){return n.recordFetch(e.that,e.result,e.args[0],e.args[1])}))},e.prototype.recordFetch=function(e,t,n,r){var i,o="GET",s="",a={};if("string"==typeof n?s=n:"url"in n?(s=n.url,o=n.method,i=n.body,n.headers&&n.headers.forEach(function(e,t){a[e]=t})):s=""+n,r){o=r.method||o;var u=r.headers;if(u)if(nt(u))for(var c=0,h=u;c-1;n&&o?t.clone().text().then(Mt.wrap(function(i){var o=mi(i,n),s=o[0],a=o[1];r.onComplete(e,t,s,a)}))["catch"](Mt.wrap(function(n){r.onComplete(e,t,-1,void 0)})):r.onComplete(e,t,-1,void 0)}))["catch"](Mt.wrap(function(t){r.onComplete(e,t,-1,void 0)}))},e.prototype.onComplete=function(e,t,n,r){var i=this,o=-1,s="";if("headers"in t){o=t.status;s=this.serializeFetchHeaders(t.headers,function(e){return i._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])})}return this._requestTracker.onComplete(e,s,o,n,r)},e.prototype.serializeFetchHeaders=function(e,t){var n="";return e.forEach(function(e,r){r=r.toLowerCase();var i=t([r,e]);n+=r+(i?": "+e:"")+di}),n},e}(),ci=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._xhrOpenHook&&(this._xhrOpenHook.disable(),this._xhrOpenHook=null),this._xhrSetHeaderHook&&(this._xhrSetHeaderHook.disable(),this._xhrSetHeaderHook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=(null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.XMLHttpRequest)||e.XMLHttpRequest;if(i){var o=i.prototype;this._xhrOpenHook=Tt(o,"open"),this._xhrOpenHook&&this._xhrOpenHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addPendingReq(e.that,t,r),e.that.addEventListener("load",Mt.wrap(function(t){n.onComplete(e.that)})),e.that.addEventListener("error",Mt.wrap(function(t){n.onComplete(e.that)}))}),this._xhrSendHook=Tt(o,"send"),this._xhrSendHook&&this._xhrSendHook.before(function(e){var t=e.args[0];n._requestTracker.addRequestBody(e.that,t)}),this._xhrSetHeaderHook=Tt(o,"setRequestHeader"),this._xhrSetHeaderHook&&this._xhrSetHeaderHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addHeader(e.that,t,r)})}},e.prototype.onComplete=function(e){var t=this,n=this.responseBody(e),r=n[0],i=n[1],o=Ei(function(e){var t=[];return e.split(di).forEach(function(e){var n=e.indexOf(":");-1!=n?t.push([e.slice(0,n).trim(),e.slice(n+1,e.length).trim()]):t.push([e.trim(),null])}),t}(e.getAllResponseHeaders()),function(e){return t._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])});return this._requestTracker.onComplete(e,o,e.status,r,i)},e.prototype.responseBody=function(e){var t=this._requestTracker.pendingReq(e);if(!t)return[-1,void 0];var n=this._requestTracker.getRspWhitelist(t.url);if(e.responseType){var r=e.response;switch(r||o("Maybe response type was different that expected."),e.responseType){case"text":return mi(e.responseText,n);case"json":return function(e,t){if(!e)return[-1,void 0];return[yi(e),ai(e,te.MaxPayloadLength,t)]}(r,n);case"arraybuffer":return function(e,t){return[e?e.byteLength:-1,t?"[ArrayBuffer]":void 0]}(r,n);case"blob":return function(e,t){return[e?e.size:-1,t?"[Blob]":void 0]}(r,n);case"document":return function(e,t){return[-1,t?"[Document]":void 0]}(0,n);}}return mi(e.responseText,n)},e}();var hi,di="\r\n",li=["a-im","accept","accept-charset","accept-encoding","accept-language","accept-datetime","access-control-request-method,","access-control-request-headers","cache-control","connection","content-length","content-md5","content-type","date","expect","forwarded","from","host","if-match","if-modified-since","if-none-match","if-range","if-unmodified-since","max-forwards","origin","pragma","range","referer","te","user-agent","upgrade","via","warning"],pi=["access-control-allow-origin","access-control-allow-credentials","access-control-expose-headers","access-control-max-age","access-control-allow-methods","access-control-allow-headers","accept-patch","accept-ranges","age","allow","alt-svc","cache-control","connection","content-disposition","content-encoding","content-language","content-length","content-location","content-md5","content-range","content-type","date","delta-base","etag","expires","im","last-modified","link","location","permanent","p3p","pragma","proxy-authenticate","public-key-pins","retry-after","permanent","server","status","strict-transport-security","trailer","transfer-encoding","tk","upgrade","vary","via","warning","www-authenticate","x-frame-options"],fi={BM7A6:["x-b3-traceid"],KD87S:["transactionid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],R16RC:["x-request-id"],DE9CX:["x-client","x-client-id","ot-baggage-original-client","x-req-id","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampling-priority"]},vi={"thefullstory.com":["x-cloud-trace-context"],TN1:["x-cloud-trace-context"],KD87S:["transactionid"],PPE96:["x-b3-traceid"],HWT6H:["x-b3-traceid"],PPEY7:["x-b3-traceid"],PPK3W:["x-b3-traceid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],NK5T9:["traceid","requestid"]},_i=function(){function e(e,t){this._ctx=e,this._queue=t,this._enabled=!1,this._tracker=new gi(e,t),this._xhr=new ci(this._tracker),this._fetch=new ui(this._tracker)}return e.prototype.isEnabled=function(){return this._enabled},e.prototype.enable=function(e){this._enabled||(this._enabled=!0,this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Ajax,!0,Y.AjaxFetch,!!e]}),this._xhr.enable(this._ctx.window),e&&this._fetch.enable(this._ctx.window))},e.prototype.disable=function(){this._enabled&&(this._enabled=!1,this._xhr.disable(),this._fetch.disable())},e.prototype.tick=function(e){this._tracker.tick(e)},e.prototype.setWatches=function(e){this._tracker.setWatches(e)},e}(),gi=function(){function e(e,t){this._ctx=e,this._queue=t,this._reqHeaderWhitelist={},this._rspHeaderWhitelist={},this._pendingReqs={},this._events=[],this._curId=1,this.addHeaderWhitelist(li,pi),this.addHeaderWhitelist(fi[e.options.orgId],vi[e.options.orgId])}return e.prototype.getReqWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._reqWhitelist[t]},e.prototype.getRspWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._rspWhitelist[t]},e.prototype.isHeaderInRequestHeaderWhitelist=function(e){return e in this._reqHeaderWhitelist},e.prototype.isHeaderInResponseHeaderWhitelist=function(e){return e in this._rspHeaderWhitelist},e.prototype.pushEvent=function(e){this._events.push(e)},e.prototype.setWatches=function(e){var t=this,n=[];this._reqWhitelist=[],this._rspWhitelist=[],e.forEach(function(e){n.push(e.URLRegex),t._reqWhitelist.push(Si(e.RecordReq,e.ReqWhitelist)),t._rspWhitelist.push(Si(e.RecordRsp,e.RspWhitelist))}),this._reqBodyRegex=new RegExp("("+n.join(")|(")+")")},e.prototype.addHeaderWhitelist=function(e,t){var n=this;e&&e.forEach(function(e){return n._reqHeaderWhitelist[e]=!0}),t&&t.forEach(function(e){return n._rspHeaderWhitelist[e]=!0})},e.prototype.tick=function(e){if(e){for(var t=0;te.MaxRuleBytes&&(o("CSSRule too large, inserting dummy instead: "+n.length),n="dummy {}"),this.withEventQueueForSheet(t,function(e){return e.enqueue({Kind:R.CSSRULE_INSERT,Args:"number"==typeof r?[i,[n],r]:[i,[n]]})}))},e.prototype.addDelete=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.CSSRULE_DELETE,Args:[n,t]})})},e.prototype.onDisableSheet=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.DISABLE_STYLESHEET,Args:[n,!!t]})})},e.prototype.withEventQueueForSheet=function(e,t){if(e.ownerNode)return n=this.ctx,r=e.ownerNode,i=t,void((o=I(Ai(r)||n.window))&&"function"==typeof o._withEventQueue&&o._withEventQueue(n.recording.pageSignature(),function(e){i({enqueue:function(t){Ft(null!=e,Ri)&&e.enqueue(t)},enqueueFirst:function(t){Ft(null!=e,Ri)&&e.enqueueFirst(t)}}),e=null}));var n,r,i,o;t(this.queue)},e.prototype.stop=function(){this.throttle.close();for(var e=0,t=this.hooks;e0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]-1)return[n];if("srcset"==t&&("img"==i||"source"==i)){return null!=n.match(/^\s*$/)?[]:n.split(",").map(function(e){return e.trim().split(/\s+/)[0]})}var o=e;if("style"==t&&o.style){var s=o.style.backgroundImage;if(!s)return[];if(s.length>300)return[];var a=[],u=void 0;for(jt.lastIndex=0;u=jt.exec(s);){var c=u[1];c&&a.push(c.trim())}return a}return[]}(e,t,n);r5e5)){var n=ki(Ti(e));if(n){if(n.length>0&&Li.test(t))return 0;var r,i=Gn();ae?(r=i.createElement("style")).textContent=e.textContent:r=i.importNode(e,!0),i.head.appendChild(r);var o=ki(Ti(r));if(i.head.removeChild(r),o)return n.length>o.length?o.length:void 0}}}(o);void 0!==s&&t.push(function(){n._styleSheetWatcher.snapshotEl(o,s)});break;default:"#"!==e.nodeName[0]&&e.nodeName.indexOf("-")>-1&&this._customElementWatcher.onCustomNodeVisited(e);}if("scrollLeft"in e&&"scrollTop"in e){var a=e;this._ctx.measurer.requestMeasureTask(function(){0==a.scrollLeft&&0==a.scrollTop||n.addScroll(a)})}},e.prototype.isSafePointerEvent=function(e){var t=Ki(e);return!!Mn(t)&&!Cn(t)},e.prototype.addMouseMove=function(e){var t=Mn(Ki(e));this._queue.enqueue({Kind:R.MOUSEMOVE,Args:t?[e.clientX,e.clientY,t]:[e.clientX,e.clientY]})},e.prototype.addMouseDown=function(e){this._queue.enqueue({Kind:R.MOUSEDOWN,Args:[e.clientX,e.clientY]})},e.prototype.addMouseUp=function(e){this._queue.enqueue({Kind:R.MOUSEUP,Args:[e.clientX,e.clientY]})},e.prototype.addTouchEvent=function(e,t){if(void 0!==e.changedTouches)for(var n=0;n0)return n[0]}}return e.target}var Vi=/^\s*at .*(\S+\:\d+|native|())/m,zi=/^(eval@)?(\[native code\])?$/;function Yi(e){if(!e||"string"!=typeof e.stack)return[];var t=e;return t.stack.match(Vi)?t.stack.split("\n").filter(function(e){return!!e.match(Vi)}).map(function(e){e.indexOf("(eval ")>-1&&(e=e.replace(/eval code/g,"eval").replace(/(\(eval at [^\()]*)|(\)\,.*$)/g,""));var t=e.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/\(native code\)/,"").split(/\s+/).slice(1),n=Qi(t.pop());return Gi(t.join(" "),["eval",""].indexOf(n[0])>-1?"":n[0],n[1],n[2])}):function(e){return e.split("\n").filter(function(e){return!e.match(zi)}).map(function(e){if(e.indexOf(" > eval")>-1&&(e=e.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g,":$1")),-1===e.indexOf("@")&&-1===e.indexOf(":"))return[e,"",-1,-1];var t=e.split("@"),n=Qi(t.pop());return Gi(t.join("@"),n[0],n[1],n[2])})}(t.stack)}function Gi(e,t,n,r){return[e||"",t||"",parseInt(n||"-1"),parseInt(r||"-1")]}function Qi(e){if(!e||-1===e.indexOf(":"))return["","",""];var t=/(.+?)(?:\:(\d+))?(?:\:(\d+))?$/.exec(e.replace(/[\(\)]/g,""));return t?[t[1]||"",t[2]||"",t[3]||""]:["","",""]}var Xi=function(){for(var e=0,t=0,n=arguments.length;t0&&"string"==typeof e.nodeName}(t)?function(e){return e.toString()}(t):void 0===t?"undefined":"object"!=typeof t||null==t?t:t instanceof Error?t.stack||t.name+": "+t.message:void 0;if(void 0!==o)return void 0===(o=s.jsonStringify(o))?0:("\""==o[0]&&(o=to(o,n,"...\"")),o.length<=n?(i.tokens.push(o),o.length):0);if(i.cyclic){i.opath.splice(r);var a=i.opath.lastIndexOf(t);if(a>-1){var u="";return u="\""+to(u,n-2)+"\"",i.tokens.push(u),u.length}i.opath.push(t)}var c=n,h=function(e){return c>=e.length&&(c-=e.length,i.tokens.push(e),!0)},d=function(e){","==i.tokens[i.tokens.length-1]?i.tokens[i.tokens.length-1]=e:h(e)};if(c<2)return 0;if(nt(t)){h("[");for(var l=0;l0;l++){var p=e(t[l],c-1,r+1,i);if(c-=p,0==p&&!h("null"))break;h(",")}d("]")}else{h("{");var f=Ze(t);for(l=0;l0;l++){var v=f[l],_=t[v];if(!h("\""+v+"\":"))break;if(0==(p=e(_,c-1,r+1,i))){i.tokens.pop();break}c-=p,h(",")}d("}")}return n==1/0?1:n-c}(e,t,0,i);var o=i.tokens.join("");return r?function(e,t){var n=t.replace(vr,"");return n=n.replace(_r,function(t){return ur(t,e,{source:"log",type:"debug"})})}(n,o):o}catch(e){return mt(e)}}function Zi(e,t){var n=0;try{s.jsonStringify(e,function(e,r){if(n++>t)throw"break";if("object"==typeof r)return r})}catch(e){return"break"!=e}return!1}var eo=function(e){return isNaN(e)?"Invalid Date":e.toUTCString()},to=function(e,t,n){return void 0===n&&(n="..."),e.length<=t?e:e.length<=n.length||t<=n.length?e.substring(0,t):e.substring(0,t-n.length)+n};var no=function(){for(var e=0,t=0,n=arguments.length;tthis._curveEndMs&&(this._curveEndMs=e.When),this._evts.push(e)},e.prototype.finish=function(e,t){void 0===t&&(t=[]);var n=this._evts.length;if(n<=1)return!1;for(var r=no([this._curveEndMs],t),i=this._evts[0].When,o=this._evts[n-1].When,s=0;s0?this._lastWhen:this._ctx.time.now();this.enqueueAt(t,e),Zt.checkForBrokenSchedulers()},e.prototype.enqueueAt=function(e,t){if(!this._recordingDisabled){e0){var t=e;t.When=this._eventQueue[0].When,this._eventQueue.unshift(t)}else this.enqueue(e)},e.prototype.addUnload=function(e){this._gotUnload||(this._gotUnload=!0,this.enqueue({Kind:R.UNLOAD,Args:[e]}),this.singSwanSong())},e.prototype.shutdown=function(e){this._flush(),this.addUnload(e),this._flush(),this._recordingDisabled=!0,this.stopPipeline()},e.prototype._flush=function(){this.processEvents(),this._transport.flush()},e.prototype.singSwanSong=function(){this._recordingDisabled||(this.processEvents(),this._transport.singSwanSong())},e.prototype.rebaseIframe=function(e){for(var t=0,n=this._eventQueue.length;t0){var d=h[h.length-1].Args[2];if(d)h[0].Args[9]=d}}for(var l in o){o[p=parseInt(l)].finish(R.SCROLL_LAYOUT_CURVE,[p])}for(var l in s){s[p=parseInt(l)].finish(R.SCROLL_VISUAL_OFFSET_CURVE,[p])}for(var l in i){var p;i[p=parseInt(l)].finish(R.TOUCHMOVE_CURVE,[p])}return t&&t.finish(R.RESIZE_VISUAL_CURVE),n}(t);e||(n=n.concat(this._gatherExternalEvents(0!=n.length))),this.ensureFrameIds(n),0!=n.length&&this._transport.enqueueEvents(this._ctx.recording.pageSignature(),n)}},e.prototype.ensureFrameIds=function(e){if(this._frameId)for(var t=this._parentIds,n=t&&t.length>0,r=0;r>>0).toString(16)).slice(-8);return e},e}();function uo(e){var t=new ao(1);return t.writeAscii(e),t.sumAsHex()}function co(e){var t=new Uint8Array(e);return lo(String.fromCharCode.apply(null,t))}var ho="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";function lo(e){var t;return(null!==(t=window.btoa)&&void 0!==t?t:po)(e).replace(/\+/g,"-").replace(/\//g,"_")}function po(e){for(var t=String(e),n=[],r=0,i=0,o=0,s=ho;t.charAt(0|o)||(s="=",o%1);n.push(s.charAt(63&r>>8-o%1*8))){if((i=t.charCodeAt(o+=.75))>255)throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");r=r<<8|i}return n.join("")}var fo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},vo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]go?[4,t(mo)]:[3,3]:[3,5];case 2:u.sent(),i=e.now(),u.label=3;case 3:a=new Uint8Array(n,s,Math.min(o-s,_o)),r.write(a),u.label=4;case 4:return s+=_o,[3,1];case 5:return[2,{hash:r.sum(),hasher:r}];}})})}var wo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},bo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]So){var i=ur(e,t,{source:"log",type:"bugsnag"});return Mt.sendToBugsnag("Size of blob resource exceeds limit","warning",{url:i,MaxResourceSizeBytes:So}),void r(null)}(function(e){var t=oo(),n=t.resolve,r=t.promise,i=new FileReader;return i.readAsArrayBuffer(e),i.onload=function(){n(i.result)},i.onerror=function(e){Mt.sendToBugsnag(e,"error"),n(null)},r})(n).then(function(e){r(e?{buffer:e,blob:n,contentType:n.type}:null)})},s.send(),i)}function ko(e,t){var n,r;return wo(this,void 0,et,function(){var i;return bo(this,function(o){switch(o.label){case 0:return i=e.window,(null===(r=null===(n=i.crypto)||void 0===n?void 0:n.subtle)||void 0===r?void 0:r.digest)?[4,i.crypto.subtle.digest({name:"sha-1"},t)]:[3,2];case 1:return[2,{hash:co(o.sent()),algorithm:"sha1"}];case 2:return[4,yo(e.time,so,t)];case 3:return[2,{hash:o.sent().hash,algorithm:"fsnv"}];}})})}var Io=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Co=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]0&&(this._regexes=this._joinRegexes(t))},e.prototype.matches=function(e){return!!this._regexes&&this._regexes.test(e)},e.prototype._isValidRegex=function(e){try{return new RegExp(e),!0}catch(t){return Mt.sendToBugsnag("Browser rejected UrlKeep.Regex","error",{expr:e,error:t.toString()}),!1}},e.prototype._joinRegexes=function(e){try{return new RegExp("("+e.join(")|(")+")","i")}catch(t){return Mt.sendToBugsnag("Browser rejected joining UrlKeep.Regexs","error",{exprs:e,error:t.toString()}),null}},e}();function Po(e,t){var n=Mn(e)+" ";return e.id&&(n+="#"+e.id),n+="[src="+ur(e.src,t,{source:"log",type:"debug"})+"]"}var qo,Uo=function(e){var t=e.transport,n=e.frame,r=e.orgId,s=e.scheme,a=e.script,u=e.recHost,c=e.appHost,h=Po(n,r);o("Injecting into Frame "+h);try{if(function(e){return e.id==e.name&&No.test(e.id)}(n))return void o("Blacklisted iframe: "+h);if(function(e){if(!e.contentDocument||!e.contentWindow||!e.contentWindow.location)return!0;return function(e){return!!e.src&&"about:blank"!=e.src&&e.src.indexOf("javascript:")<0}(e)&&e.src!=e.contentWindow.location.href&&"loading"==e.contentDocument.readyState}(n))return void o("Frame not yet loaded: "+h);var d=n.contentWindow,l=n.contentDocument;if(!d||!l)return void o("Missing contentWindow or contentDocument: "+h);if(!l.head)return void o("Missing contentDocument.head: "+h);if(I(d))return void o("FS already defined in Frame contentWindow: "+h+". Ignoring.");d._fs_org=r,d._fs_script=a,d._fs_rec_host=u,d._fs_app_host=c,d._fs_debug=i(),d._fs_run_in_iframe=!0,t&&(d._fs_transport=t);var p=l.createElement("script");p.async=!0,p.crossOrigin="anonymous",p.src=s+"//"+a,"testdrive"==r&&(p.src+="?allowMoo=true"),l.head.appendChild(p)}catch(e){o("iFrame no injecty. Probably not same origin.")}},No=/^fb\d{18}$/;!function(e){e[e.NoInfoYet=1]="NoInfoYet",e[e.Enabled=2]="Enabled",e[e.Disabled=3]="Disabled"}(qo||(qo={}));var Wo=function(){function e(e,t,n,r){var i=this;this._ctx=e,this._transport=n,this._injector=r,this._bundleUploadInterval=te.DefaultBundleUploadInterval,this._iFrames=[],this._pendingChildFrameIdInits=[],this._listeners=new Ut,this._getCurrentSessionEnabled=qo.NoInfoYet,this._resourceUploadingEnabled=!1,this._tickerTasks=[],this._pendingIframes={},this._watcher=new yn,this._queue=new io(e,this._transport,function(e){for(var t=i._eventWatcher.bundleEvents(e),n=void 0;n=i._tickerTasks.pop();)n();return t},t),this._keep=new Lo(e,this._queue),this._eventWatcher=new Di(e,this._queue,this._keep,this._watcher,this._listeners,function(e){i.onFrameCreated(e)},function(e){i.beforeFrameRemoved(e)},new Eo(e,this._queue,new Ao(e))),this._consoleWatcher=new Ji(e,this._queue,this._listeners),this._scheme=e.options.scheme,this._script=e.options.script,this._recHost=e.options.recHost,this._appHost=e.options.appHost,this._orgId=e.options.orgId,this._wnd=e.window}return e.prototype.bundleUploadInterval=function(){return this._bundleUploadInterval},e.prototype.start=function(e,t){var n=this;this._onFullyStarted=t,"onpagehide"in this._wnd?this._listeners.add(this._wnd,"pagehide",!1,function(e){n.onUnload()}):this._listeners.add(this._wnd,"unload",!1,function(e){n.onUnload()}),this._listeners.add(this._wnd,"message",!1,function(e){if("string"==typeof e.data){var t=e.source;n.postMessageReceived(t,Bo(e.data))}});var r=this._wnd.Document?this._wnd.Document.prototype:this._wnd.document;this._docCloseHook=Tt(r,"close"),this._docCloseHook&&this._docCloseHook.afterAsync(function(){n._listeners.refresh()})},e.prototype.queue=function(){return this._queue},e.prototype.eventWatcher=function(){return this._eventWatcher},e.prototype.console=function(){return this._consoleWatcher},e.prototype.onDomLoad=function(){this._eventWatcher.onDomLoad()},e.prototype.onLoad=function(){this._eventWatcher.onLoad()},e.prototype.shutdown=function(e){this._eventWatcher.shutdown(e),this._consoleWatcher.disable(),this._listeners&&this._listeners.clear(),this._docCloseHook&&this._docCloseHook.disable(),this.tellAllFramesTo(["ShutdownFrame"])},e.prototype.tellAllFramesTo=function(e){for(var t=0;t0){for(var e=0;e0&&this._transport.enqueueEvents(r,n);break;case"RequestFrameId":if(!e)return void o("No MessageEvent.source, iframe may have unloaded.");var s=this.iFrameWndToFsId(e);s?(o("Responding to FID request for frame "+s),this._pendingIframes[s]=!1,this.sendFrameIdToInnerFrame(e,s)):o("No FrameId found. Hoping to send one later.");}},e.prototype.sendFrameIdToInnerFrame=function(e,t){var n=this,r=function(){var r=[];0!=n._frameId&&(r=n._parentIds?n._parentIds.concat(n._frameId):[n._frameId]);var i=n._ctx.time.startTime();Do(e,["SetFrameId",t,r,i,n._scheme,n._script,n._appHost,n._orgId,n._pageRsp])};null==this._frameId?this._pendingChildFrameIdInits.push(r):r()},e.prototype.iFrameWndToFsId=function(e){for(var t=0;t=400&&502!==e||202==e||206==e}var jo=function(){return(jo=Object.assign||function(e){for(var t,n=1,r=arguments.length;n2e6))try{localStorage._fs_swan_song=t}catch(e){}},e.prototype._recover=function(){try{if("_fs_swan_song"in localStorage){var e=localStorage._fs_swan_song||localStorage.singSwanSong;delete localStorage._fs_swan_song,delete localStorage.singSwanSong;var t=wt(e);if(!(t.Bundles&&t.UserId&&t.SessionId&&t.PageId))return void o("Malformed swan song found. Ignoring it.");t.OrgId||(t.OrgId=this._identity.orgId()),t.Bundles.length>0&&(o("Sending "+t.Bundles.length+" bundles as prior page swan song"),this.sendSwanSongBundles(t))}}catch(e){o("Error recovering swan-song: "+e)}},e.prototype.sendSwanSongBundles=function(e,t){var n=this;void 0===t&&(t=0);var r=null;if(nt(e.Bundles)&&0!==e.Bundles.length&&void 0!==e.Bundles[0]){1==e.Bundles.length&&(r=this._ctx.time.wallTime()-(e.LastBundleTime||0));this._protocol.bundle({bundle:e.Bundles[0],deltaT:r,orgId:e.OrgId,pageId:e.PageId,serverBundleTime:e.ServerBundleTime,serverPageStart:e.ServerPageStart,sessionId:e.SessionId,userId:e.UserId,isNewSession:e.IsNewSession,win:function(t){o("Sent "+e.Bundles[0].Evts.length+" trailing events from last session as Seq "+e.Bundles[0].Seq),e.Bundles.shift(),e.Bundles.length>0?n.sendSwanSongBundles(jo(jo({},e),{ServerBundleTime:t.BundleTime})):o("Done with prior page swan song")},lose:function(r){Ho(r)?o("Fatal error while sending events, giving up"):(o("Failed to send events from last session, will retry while on this page"),n._lastSwanSongRetryTimeout=new n._timeoutFactory(n.sendSwanSongBundles,n._protocol.exponentialBackoffMs(t,!0),n,e,t+1).start())}})}},e}(),Vo=function(){function e(e,t,n,r){var i=this;void 0===t&&(t=new Ro(e)),void 0===n&&(n=en),void 0===r&&(r=tn),this._ctx=e,this._protocol=t,this._tickerFactory=n,this._backoffRetries=0,this._backoffTime=0,this._bundleSeq=1,this._lastPostTime=0,this._serverBundleTime=0,this._isNewSession=!1,this._largePageSize=16e6,this._outgoingEventQueue=[],this._bundleQueue=[],this._hibernating=!1,this._heartbeatInterval=0,this._lastUserActivity=this._ctx.time.wallTime(),this._finished=!1,this._scheme=e.options.scheme,this._identity=e.recording.identity,this._lastBundleTime=e.time.wallTime(),this._swanSong=new Ko(e,this._protocol,this._identity,r),this._heartbeatTimeout=new r(function(){i.onHeartbeat()}),this._hibernationTimeout=new r(function(){i.onHibernate()},te.PageInactivityTimeout)}return e.prototype.onShutdown=function(e){this._onShutdown=e},e.prototype.scheme=function(){return this._scheme},e.prototype.enqueueEvents=function(e,t){if(this.maybeHibernate(),this._hibernating){if(this._finished)return;for(var n=0,r=t;n0&&this.enqueueNextBundle(!0),this._bundleQueue.length>0||this._pendingBundle)){var e=this._bundleQueue.concat();this._pendingBundle&&e.unshift(this._pendingBundle),this._swanSong.sing({pageId:this._pageId,bundles:e,lastBundleTime:this._lastBundleTime,serverPageStart:this._serverPageStart,serverBundleTime:this._serverBundleTime,isNewSession:this._isNewSession})}},e.prototype.maybeHibernate=function(){this._hibernating||this.calcLastUserActivityDuration()>=te.PageInactivityTimeout+5e3&&this.onHibernate()},e.prototype.calcLastUserActivityDuration=function(){return s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity)},e.prototype.onHeartbeat=function(){var e=this.calcLastUserActivityDuration();this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.HEARTBEAT,Args:[e]}),this._heartbeatInterval*=2,this._heartbeatInterval>te.HeartbeatMax&&(this._heartbeatInterval=te.HeartbeatMax),this._heartbeatTimeout.start(this._heartbeatInterval)},e.prototype.onHibernate=function(){this._hibernating||(this.calcLastUserActivityDuration()<=2*te.PageInactivityTimeout&&(this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.UNLOAD,Args:[V.Hibernation]}),this.singSwanSong()),this.stopPipeline(),this._hibernating=!0)},e.prototype.enqueueAndSendBundle=function(){this._pendingBundle?this._pendingBundleFailed&&this._sendPendingBundle():0!=this._outgoingEventQueue.length?this.enqueueNextBundle():this.maybeSendNextBundle()},e.prototype.enqueueNextBundle=function(e){void 0===e&&(e=!1);var t={When:this._outgoingEventQueue[0].When,Seq:this._bundleSeq++,Evts:this._outgoingEventQueue};this._outgoingEventQueue=[],this._bundleQueue.push(t),e?this._protocol.bundleBeacon({bundle:t,deltaT:null,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:function(){},lose:function(){}}):this.maybeSendNextBundle()},e.prototype.maybeSendNextBundle=function(){this._pageId&&this._serverPageStart&&!this._pendingBundle&&0!=this._bundleQueue.length&&(this._pendingBundle=this._bundleQueue.shift(),this._sendPendingBundle())},e.prototype._sendPendingBundle=function(){var e=this,t=this._ctx.time.wallTime();if(!(te._ctx.recording.bundleUploadInterval()&&e.maybeSendNextBundle()},function(t){if(o("Failed to send events."),Ho(t))return 206==t?Mt.sendToBugsnag("Failed to send bundle, probably because of its large size","error"):t>=500&&Mt.sendToBugsnag("Failed to send bundle, recording outage likely","error"),void(e._onShutdown&&e._onShutdown());e._pendingBundleFailed=!0,e._backoffTime=e._lastPostTime+e._protocol.exponentialBackoffMs(e._backoffRetries++,!1)}))}},e.prototype.sendBundle=function(e,t,n){var r=s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity),i=this._protocol.bundle({bundle:e,deltaT:null,lastUserActivity:r,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:t,lose:n});i>this._largePageSize&&this._bundleSeq>16&&(o("splitting large page: "+i),this._ctx.recording.splitPage(V.Size))},e}(),zo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),Yo=function(e){function t(t,n,r,i,o){void 0===r&&(r=new Vo(t,n)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s,a,u=e.call(this,t,i,r,o)||this;return u._protocol=n,u._domLoaded=!1,u._recordingDisabled=!1,u._integrationScriptFetched=!1,r.onShutdown(function(){return u.shutdown(V.SettingsBlocked)}),u._doc=u._wnd.document,u._frameId=0,u._identity=t.recording.identity,u._getCurrentSessionEnabled=qo.NoInfoYet,s=u._wnd,a=function(e){if(u._eventWatcher.shutdown(V.Api),e){var t=u._doc.getElementById(e);t&&t.setAttribute("_fs_embed_token",u._embedToken)}},s._fs_shutdown=a,u}return zo(t,e),t.prototype.onDomLoad=function(){var t=this;e.prototype.onDomLoad.call(this),this._domLoaded=!0,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.getReplayFlags=function(){var e=U(this._wnd);if(/[?&]_fs_force_session=true(&|#|$)/.test(location.search)&&(e+=",forceSession",this._wnd.history)){var t=location.search.replace(/(^\?|&)_fs_force_session=true(&|$)/,function(e,t,n){return n?t:""});this._wnd.history.replaceState({},"",this._wnd.location.href.replace(location.search,t))}return e},t.prototype.start=function(t,n){var r,i,o,s=this;e.prototype.start.call(this,t,n);var a=this.getReplayFlags(),u=Vt(this._doc),c=u[0],h=u[1],d=bt(this._wnd),l=d[0],p=d[1],f="";t||(f=this._identity.userId());var v=null!==(o=null===(i=null===(r=this._ctx)||void 0===r?void 0:r.recording)||void 0===i?void 0:i.preroll)&&void 0!==o?o:-1,_=ur(Jn(this._wnd),this._orgId,{source:"page",type:"base"}),g=ur(this._doc.referrer,this._orgId,{source:"page",type:"referrer"}),m=ur(this._wnd.location.href,this._orgId,{source:"page",type:"url"}),y={OrgId:this._orgId,UserId:f,Url:m,Base:_,Width:c,Height:h,ScreenWidth:l,ScreenHeight:p,Referrer:g,Preroll:v,Doctype:yt(this._doc),CompiledTimestamp:1591209308,AppId:this._identity.appId()};a&&(y.ReplayFlags=a),this._protocol.page(y,function(e){s.handleResponse(e),s.handleIdentity(e.CookieDomain,e.UserIntId,e.SessionIntId,e.PageIntId,e.EmbedToken),s.handleIntegrationScript(e.IntegrationScript),e.PreviewMode&&s.maybeInjectPreviewScript();var t=s._wnd._fs_pagestart;t&&t();var n=!!e.Consented;s._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[n,K.Document]}),s._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(s._wnd),s._orgId,{source:"event",type:R.SET_FRAME_BASE}),yt(s._doc)]}),s._queue.startPipeline({pageId:e.PageIntId,serverPageStart:e.PageStart,isNewSession:!!e.IsNewSession}),s.fullyStarted()},function(e,t){t&&t.user_id&&t.cookie_domain&&t.reason_code==$.ReasonBlockedTrafficRamping&&f!=t.user_id&&s.handleIdentity(t.cookie_domain,t.user_id,"","",""),s.disableBecauseRecPageSaidSo()})},t.prototype.handleIntegrationScript=function(e){var t=this;this._integrationScriptFetched=!0,this._integrationScript=e,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.handleIdentity=function(e,t,n,r,i){var s=this._identity;s.setIds(this._wnd,e,t,n),this._embedToken=i,o("/User,"+s.userId()+"/Session,"+s.sessionId()+"/Page,"+r)},t.prototype.injectIntegrationScript=function(e){if(this._domLoaded&&this._integrationScriptFetched)if(this._integrationScript){var t=this._doc.createElement("script");this._wnd._fs_csp?(t.addEventListener("load",e),t.addEventListener("error",e),t.async=!0,t.src=this._scheme+"//"+this._recHost+"/rec/integrations?OrgId="+this._orgId,this._doc.head.appendChild(t)):(t.text=this._integrationScript,this._doc.head.appendChild(t),e())}else e()},t.prototype.maybeInjectPreviewScript=function(){if(!this._doc.getElementById("FullStory-preview-script")){var e=this._doc.createElement("script");e.id="FullStory-preview-script",e.async=!0,e.src=this._scheme+"//"+this._appHost+"/s/fspreview.js",this._doc.head.appendChild(e)}},t.prototype.disableBecauseRecPageSaidSo=function(){this.shutdown(V.SettingsBlocked),o("Disabling FS."),this._recordingDisabled=!0,this.fireFsReady(this._recordingDisabled)},t}(Wo),Go=function(){function e(e,t){void 0===t&&(t=new Qo(e)),this._wnd=e,this._messagePoster=t}return e.prototype.enqueueEvents=function(e,t){this._messagePoster.postMessage(this._wnd.parent,"EvtBundle",t,e)},e.prototype.startPipeline=function(){},e.prototype.stopPipeline=function(){},e.prototype.flush=function(){},e.prototype.singSwanSong=function(){},e.prototype.onShutdown=function(e){},e}(),Qo=function(){function e(e){this.wnd=e}return e.prototype.postMessage=function(e,t,n,r){var i=N(this.wnd);if(i)try{i.send(t,gt(n),r)}catch(e){i.send(t,gt(n))}else e.postMessage(function(e,t,n){var r=[e,t];n&&r.push(n);return gt({__fs:r})}(t,n,r),"*")},e}();var Xo,Jo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),$o=function(e){function t(t,n,r,i,o){void 0===n&&(n=new Qo(t.window)),void 0===r&&(r=new Go(t.window)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s=e.call(this,t,i,r,o)||this;return s._messagePoster=n,s}return Jo(t,e),t.prototype.start=function(t,n){var r=this;e.prototype.start.call(this,t,n),this.sendRequestForFrameId(),this._listeners.add(this._wnd,"load",!1,function(){r._eventWatcher.recordingIsDetached()&&(o("Recording wrong document. Restarting recording in iframe."),r._ctx.recording.splitPage(V.FsShutdownFrame))})},t.prototype.postMessageReceived=function(t,n){if(e.prototype.postMessageReceived.call(this,t,n),t==this._wnd.parent||t==this._wnd)switch(n[0]){case"GreetFrame":this.sendRequestForFrameId();break;case"SetFrameId":try{var r=n[1],i=n[2],s=n[3],a=n[4],u=n[5],c=n[6],h=n[7],d=n[8];if(!r)return void o("Outer page gave us a bogus frame Id! Iframe: "+ur(location.href,h,{source:"log",type:"debug"}));this.setFrameIdFromOutside(r,i,s,a,u,c,h,d)}catch(e){o("Failed to parse frameId from message: "+gt(n))}break;case"SetConsent":var l=n[1];this.setConsent(l);break;case"InitFrameMobile":try{var p=JSON.parse(n[1]),f=p.StartTime;if(n.length>2){var v=n[2];if(v.hasOwnProperty("ProtocolVersion"))v.ProtocolVersion>=20180723&&v.hasOwnProperty("OuterStartTime")&&(f=v.OuterStartTime)}var _=p.Host;this.setFrameIdFromOutside(0,[],f,"https:",H(_),B(_),p.OrgId,p.PageResponse)}catch(e){o("Failed to initialize mobile web recording from message: "+gt(n))}}},t.prototype.sendRequestForFrameId=function(){this._frameId||(0!=this._frameId?this._wnd.parent?(o("Asking for a frame ID."),this._messagePoster.postMessage(this._wnd.parent,"RequestFrameId",[])):o("Orphaned window."):o("For some reason the outer window attempted to request a frameId"))},t.prototype.setFrameIdFromOutside=function(e,t,n,r,i,s,a,u){if(this._frameId)this._frameId!=e?(o("Updating frame id from "+this._frameId+" to "+e),this._ctx.recording.splitPage(V.FsShutdownFrame)):o("frame Id is already set to "+this._frameId);else{o("FrameId received within frame "+ur(location.href,a,{source:"log",type:"debug"})+": "+e),this._scheme=r,this._script=i,this._appHost=s,this._orgId=a,this._frameId=e,this._parentIds=t,this.handleResponse(u),this.fireFsReady();var c=!!u.Consented;this._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[c,K.Document]}),this._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(this._wnd),a,{source:"event",type:R.SET_FRAME_BASE}),yt(this._wnd.document)]}),this._queue.rebaseIframe(n),this._ctx.time.setStartTime(n),this._queue.startPipeline({pageId:this._pageId,serverPageStart:u.PageStart,isNewSession:!!u.IsNewSession,frameId:e,parentIds:t}),this.flushPendingChildFrameInits(),this.fullyStarted()}},t}(Wo),Zo="fsidentity",es="newuid",ts=function(){function e(e,t){void 0===e&&(e=document),void 0===t&&(t=function(){}),this._doc=e,this._onWriteFailure=t,this._cookies={},this._appId=void 0}return e.prototype.initFromCookies=function(e,t){this._cookies=y(this._doc);var n=this._cookies.fs_uid;if(!n)try{n=localStorage._fs_uid}catch(e){}var r=m(n);r&&r.host.replace(/^www\./,"")==e.replace(/^www\./,"")&&r.orgId==t?this._cookie=r:this._cookie={expirationAbsTimeSeconds:g(),host:e,orgId:t,userId:"",sessionId:"",appKeyHash:""}},e.prototype.initFromParsedCookie=function(e){this._cookie=e},e.prototype.clear=function(){this._cookie.userId=this._cookie.sessionId=this._cookie.appKeyHash=this._appId="",this._cookie.expirationAbsTimeSeconds=g(),this._write()},e.prototype.host=function(){return this._cookie.host},e.prototype.orgId=function(){return this._cookie.orgId},e.prototype.userId=function(){return this._cookie.userId},e.prototype.sessionId=function(){return this._cookie.sessionId},e.prototype.appKeyHash=function(){return this._cookie.appKeyHash},e.prototype.cookieData=function(){return this._cookie},e.prototype.cookies=function(){return this._cookies},e.prototype.setCookie=function(e,t,n){void 0===n&&(n=new Date(p()+6048e5).toUTCString());var r=e+"="+t;this._domain?r+="; domain=."+encodeURIComponent(this._domain):r+="; domain=",r+="; Expires="+n+"; path=/; SameSite=Strict","https:"===location.protocol&&(r+="; Secure"),this._doc.cookie=r},e.prototype.setIds=function(e,t,n,r){(C(t)||t.match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/g))&&(t="");var i=function(e){return e._fs_cookie_domain}(e);"string"==typeof i&&(t=i),this._domain=t,this._cookie.userId=n,this._cookie.sessionId=r,this._write()},e.prototype.clearAppId=function(){return!!this._cookie.appKeyHash&&(this._appId="",this._cookie.appKeyHash="",this._write(),!0)},e.prototype.setAppId=function(e){this._appId=e,this._cookie.appKeyHash=uo(e),this._write()},e.prototype.appId=function(){return this._appId},e.prototype.encode=function(){var e=this._cookie.host+"#"+this._cookie.orgId+"#"+this._cookie.userId+":"+this._cookie.sessionId;return this._cookie.appKeyHash&&(e+="#"+encodeURIComponent(this._cookie.appKeyHash)+"#"),e+="/"+this._cookie.expirationAbsTimeSeconds},e.prototype._write=function(){if(null!=this._domain){var e=this.encode(),t=new Date(1e3*this._cookie.expirationAbsTimeSeconds).toUTCString();this.setCookie("fs_uid",e,t);var n=[];-1===this._doc.cookie.indexOf(e)&&n.push(["fs_uid","cookie"]);try{localStorage._fs_uid=e,localStorage._fs_uid!==e&&n.push(["fs_uid","localStorage"])}catch(e){n.push(["fs_uid","localStorage",String(e)])}n.length>0&&this._onWriteFailure(n)}},e}();!function(e){e.rec="rec",e.user="user",e.account="account",e.consent="consent",e.customEvent="event",e.log="log"}(Xo||(Xo={}));var ns={acctId:"str",displayName:"str",website:"str"},rs={uid:"str",displayName:"str",email:"str"},is={str:os,bool:ss,real:as,"int":us,date:cs,strs:hs(os),bools:hs(ss),reals:hs(as),ints:hs(us),dates:hs(cs),objs:hs(ds),obj:ds};function os(e){return"string"==typeof e}function ss(e){return"boolean"==typeof e}function as(e){return"number"==typeof e}function us(e){return"number"==typeof e&&e-s.mathFloor(e)==0}function cs(e){return!!e&&(e.constructor===Date?!isNaN(e):("number"==typeof e||"string"==typeof e)&&!isNaN(new Date(e)))}function hs(e){return function(t){if(!(t instanceof Array))return!1;for(var n=0;n=0)return o("blocking FS.identify API call; uid value ("+n+") is illegal"),[void 0,Zo];var r=uo(n),i=void 0;t&&t._cookie.appKeyHash&&t._cookie.appKeyHash!==r&&t._cookie.appKeyHash!==n&&(o("user re-identified; existing uid hash ("+t._cookie.appKeyHash+") does not match provided uid ("+n+")"),i=es);return[n,i]}(a,this._identity),c=u[0],h=u[1];if(!c){switch(h){case Zo:case void 0:break;default:o("unexpected failReason returned from setAppId: "+h);}return{events:i}}t.uid=c,this._identity.setAppId(t.uid),h==es&&(r=!0)}}i.push.apply(i,this.rawEventsFromApi(X.User,rs,t,n));break;case Xo.customEvent:var d=t.n,l=t.p;i.push.apply(i,this.rawEventsFromApi(X.Event,{},l,n,d));break;default:o("invalid operation \""+e+"\"; only \"rec\", \"account\", and \"user\" are supported at present");}}catch(t){o("unexpected exception handling "+e+" API call: "+t.message)}return{events:i,reidentify:r}},e.prototype.rawEventsFromApi=function(e,t,n,r,i){var a=function e(t,n,r){var i={PayloadToSend:{},ValidationErrors:[]},a=function(r){var o=e(t,n,r);return i.ValidationErrors=i.ValidationErrors.concat(o.ValidationErrors),o.PayloadToSend};return ht(r,function(e,r){var u=function(e,t,n,r){var i=t,a=typeof n;if("undefined"===a)return o("Cannot infer type of "+a+" "+n),r.push({Type:"vartype",FieldName:t,ValueType:a+" (unsupported)"}),null;if(s.objectHasOwnProp(e,t))return{name:t,type:e[t]};var u=t.lastIndexOf("_");if(-1==u||!vs(t.substring(u+1))){var c=function(e){for(var t in is)if(is[t](e))return t;return null}(n);if(null==c)return o("Cannot infer type of "+a+" "+n),n?r.push({Type:"vartype",FieldName:t}):r.push({Type:"vartype",FieldName:t,ValueType:"null (unsupported)"}),null;u=t.length,o("Warning: Inferring user variable \""+t+"\" to be of type \""+c+"\""),t=t+"_"+c}var h=[t.substring(0,u),t.substring(u+1)],d=h[0],l=h[1];if("object"===a&&!n)return o("null is not a valid object type"),r.push({Type:"vartype",FieldName:i,ValueType:"null (unsupported)"}),null;if(!ls.test(d)){d=d.replace(/[^a-zA-Z0-9_]/g,"").replace(/^[0-9]+/,""),/[0-9]/.test(d[0])&&(d=d.substring(1)),r.push({Type:"varname",FieldName:i});var p=d+"_"+l;if(o("Warning: variable \""+i+"\" has invalid characters. It should match /"+ls.source+"/. Converted name to \""+p+"\"."),""==d)return null;t=p}if(!vs(l))return o("Variable \""+i+"\" has invalid type \""+l+"\""),r.push({Type:"varname",FieldName:i}),null;if(!function(e,t){return is[e](t)}(l,n))return o("illegal value "+gt(n)+" for type "+l),"number"===a?a=n%1==0?"integer":"real":"object"==a&&null!=n&&n.constructor==Date&&(a=isNaN(n)?"invalid date":"date"),r.push({Type:"vartype",FieldName:i,ValueType:a}),null;return{name:t,type:l}}(n,r,e,i.ValidationErrors);if(u){var c=u.name,h=u.type;if("obj"!=h){if("objs"!=h){var d,l;i.PayloadToSend[c]=fs(h,e)}else{t!=X.Event&&i.ValidationErrors.push({Type:"vartype",FieldName:c,ValueType:"Array (unsupported)"});for(var p=e,f=[],v=0;v0&&(i.PayloadToSend[c]=f)}}else{var _=a(e),g=(l="_obj").length>(d=r).length||d.substring(d.length-l.length)!=l?c.substring(0,c.length-"_obj".length):c;i.PayloadToSend[g]=_}}else i.PayloadToSend[r]=fs("",e)}),i}(e,t,n),u=[],c=e==X.Event,h=gt(a.PayloadToSend),d=!!r&&"fs"!==r;return c?u.push({When:0,Kind:R.SYS_CUSTOM,Args:d?[i,h,r]:[i,h]}):u.push({When:0,Kind:R.SYS_SETVAR,Args:d?[e,h,r]:[e,h]}),u},e}();function fs(e,t){return"str"==e&&null!=t&&(t=t.trim()),null==t||"date"!=e&&t.constructor!=Date||(t=function(e){var t,n=e.constructor===Date?e:new Date(e);try{t=n.toISOString()}catch(e){t=null}return t}(t)),t}function vs(e){return!!is[e]}var _s=function(){return(_s=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]u.length?u.length:a.length,d=1,l=d;l=0}var Es=["__zone_symbol__OriginalDelegate","nr@original"];function Ts(e,t){if(t){for(var n=0,r=Es;n16)Mt.sendToBugsnag("Too much synchronous recursion in requestMeasureTask","error");else{var n=this.performingMeasurements?this.recursionDepth:0,r=Mt.wrap(function(){var r=t.recursionDepth;t.recursionDepth=n+1;try{e()}finally{t.recursionDepth=r}});this.measurementTasks?this.measurementTasks.push(r):(this.measurementTasks=[r],this.schedule())}},e.prototype.performMeasurementsNow=function(){this.performMeasurements()},e}(),As=function(e){function t(t,n){var r=e.call(this)||this;return r.wnd=t,r.ResizeObserver=n,r}return Cs(t,e),t.prototype.schedule=function(){var e=this,t=this.ResizeObserver,n=this.wnd.document,r=n.body||n.documentElement||n.head,i=new t(function(){i.unobserve(r),e.performMeasurements()});i.observe(r)},t}(Rs),xs=function(e){function t(t,n,r){var i=e.call(this)||this;return i.wnd=t,i.requestWindowAnimationFrame=n,i.onRAF=Mt.wrap(function(){i.ch.port2.postMessage(void 0)}),i.ch=new r,i}return Cs(t,e),t.prototype.schedule=function(){this.ch.port1.onmessage=this.performMeasurements,this.requestWindowAnimationFrame(this.wnd,this.onRAF)},t}(Rs),Os=function(e){function t(t){var n=e.call(this)||this;return n.wnd=t,n}return Cs(t,e),t.prototype.schedule=function(){s.setWindowTimeout(this.wnd,this.performMeasurements,0)},t}(Rs),Ms=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Ls=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=8)return void o("reidentified too many times; giving up");this.reidentifyCount++,W(this.wnd,[e,t]),this.splitPage(V.Reidentify,!0)}else u();void 0!==a&&(a?this.restart():this.shutdown(V.Api))}else W(this.wnd,[e,t])},e.prototype._cookies=function(){return this.identity?this.identity.cookies():(o("Error in FS integration: Can't get cookies from inside an iframe"),null)},e.prototype._setCookie=function(e,t){this.identity?this.identity.setCookie(e,t):o("Error in FS integration: Can't set cookies from inside an iframe")},e.prototype._withEventQueue=function(e,t){if(this.recorder){var n=this.recorder.queue(),r=this.recorder.pageSignature();null!=n&&null!=r?e===r?t(n):Mt.sendToBugsnag("Error in _withEventQueue: Page Signature mismatch","error",{pageSignature:r,callerSignature:e}):o("Error getting event queue or page signature: Recorder not initialized")}else o("Error in FS integration: Recorder not initialized")},e.prototype.initApi=function(){var e=I(this.wnd);e?(e.getCurrentSessionURL=_t(this.getCurrentSessionURL,this),e.getCurrentSession=_t(this.getCurrentSession,this),e.enableConsole=_t(this.enableConsole,this),e.disableConsole=_t(this.disableConsole,this),e.log=_t(this.log,this),e.shutdown=_t(this.shutdownApi,this),e.restart=_t(this.restart,this),e._api=_t(this._api,this),e._cookies=_t(this._cookies,this),e._setCookie=_t(this._setCookie,this),e._withEventQueue=_t(this._withEventQueue,this)):o("Missing browser API namespace; couldn't initialize API.")},e.prototype.start=function(){var e,t=this;e=L(this.wnd),r=e,o("script version UNSET (compiled at 1591209308)");var n=P(this.wnd);if(n){this.orgId=n;var i,s=(i=this.wnd)._fs_script||H(D(i));if(s){this.script=s;var a=F(this.wnd);if(a){this.recHost=a;var u=function(e){return e._fs_app_host||B(D(e))}(this.wnd);u?(this.appHost=u,o("script: "+this.script),o("recording host: "+this.recHost),o("orgid: "+this.orgId),"localhost:8080"==this.recHost&&(this.scheme="http:"),this.inFrame()||(this.identity=new ts(this.wnd.document,function(e){for(var n,r=0,i=e;r=0){var s=e.split("/"),o=s[0],u=s[1];i[r]=o,n=u;break}}var a=function(t){var n=parseInt(null!=t?t:"",10),i=E(),r=S();return isNaN(n)?r:n<=i?void 0:n>r?r:n}(n);if(!a)return null;i[0];var c=i[1],h=i[2],f=i[3],v="";f&&(v=decodeURIComponent(f),(y.indexOf(v)>=0||b.indexOf(v)>=0)&&(v=""));var l=(null!=h?h:"").split(":"),d=l[0],p=l[1],w=l[2];return l[3],{appKeyHash:v,expirationAbsTimeSeconds:a,userId:d,orgId:c,pageCount:_(l[4]),sessionId:null!=p?p:"",sessionStartTime:_(w)}}function k(t){var n={};try{for(var i=t.cookie.split(";"),r=0;r1))return s}}(t);if(!i||!K(n))return n;var r="";return 0===n.indexOf("www.")&&(n=n.slice(4),r="www."),0===n.indexOf(i+".")&&(n=n.slice((i+".").length)),""+r+i+"."+n}}function $(t){return t?C(function(t){var n=t,i=n.indexOf(":");return i>=0&&(n=n.slice(0,i)),n}(t))?t:0==t.indexOf("www.")?"app."+t.slice(4):"app."+t:t}function G(t){var n=j(t);if(n)return n+"/s/fs.js"}function X(t,n){return function(){for(var i=[],r=0;rn)return!1;return i==n}function ot(t,n){var i=0;for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r)&&++i>n)return!0;return!1}function ut(t){var n=t.nextSibling;return n&&t.parentNode&&n===t.parentNode.firstChild?null:n}function at(t){var n=t.previousSibling;return n&&t.parentNode&&n===t.parentNode.lastChild?null:n}function ct(t){return function(){for(var n=this,i=[],r=0;r"}function pt(t){return o.jsonParse(t)}var wt=function(){function t(t,n,i){void 0===i&&(i=!1),this.i=t,this.u=n,this.l=i,this.g=J,this.m=J,this.S=J,this.k=!1}return t.prototype.before=function(t){return this.g=ft(t),this},t.prototype.afterSync=function(t){return this.m=ft(t),this},t.prototype.afterAsync=function(t){return this.S=ft(function(n){o.setWindowTimeout(window,X(function(){t(n)}),0)}),this},t.prototype.disable=function(){if(this.k=!1,this._){var t=this._,n=t.override,i=t["native"];this.i[this.u]===n&&(this.i[this.u]=i,this._=void 0)}},t.prototype.enable=function(){if(this.k=!0,this._)return!0;this._=this.A();try{this.i[this.u]=this._.override}catch(t){return!1}return!0},t.prototype.getTarget=function(){return this.i},t.prototype.A=function(){var t=this,n=this,i=this.i[this.u],r=function(){for(var t=[],r=0;r\n";var i=[];try{for(var r=arguments.callee.caller.caller;r&&i.length<10;){var e=kt.test(r.toString())&&RegExp.$1||xt;i.push(e),r=r.caller}}catch(t){t.toString()}n=i.join("\n")}return t+n}function It(){try{return window.self!==window.top}catch(t){return!0}}var Tt=function(){function t(){}return t.wrap=function(n,i){return void 0===i&&(i="error"),X(n,function(n){return t.sendToBugsnag(n,i)})},t.I=15,t.sendToBugsnag=function(n,i,r){if(!(t.I<=0)){t.I--;var e=n;"string"==typeof e&&(e=new Error(e));var s=k(document).fs_uid,o=s?x(s):void 0;o&&o.orgId!=F(window)&&(o=void 0);var u=new Date(1678707725e3).toISOString(),a={projectRoot:window.location.origin,deviceTime:p(),inIframe:It(),CompiledVersion:"11aa377d19",CompiledTimestamp:1678707725,CompiledTime:u,orgId:F(window),"userId:sessionId":o?o.userId+":"+o.sessionId:"NA",context:document.location&&document.location.pathname,message:e.message,name:"Recording Error",releaseStage:"production "+u,severity:i,language:Et(window),stacktrace:_t(e)||At()},c=function(t,n,i){var r=encodeURIComponent(n)+"="+encodeURIComponent(i);t.push(r)},h=[];for(var f in a)c(h,f,a[f]||"");if(r)for(var f in r)c(h,"aux_"+f,Ct(r[f]));new Image().src="https://"+L(window)+"/rec/except?"+h.join("&")}},t}();function Ct(t){try{var n=typeof t+": "+vt(t);return"function"==typeof t.toString&&(n+=" (toString: "+t.toString()+")"),n}catch(t){return"failed to serialize \""+(null==t?void 0:t.message)+"\""}}var Pt={};function jt(t,n,i){if(void 0===i&&(i=1),t)return!0;if(Pt[n]=Pt[n]||0,Pt[n]++,Pt[n]>i)return!1;var r=new Error("Assertion failed: "+n);return Tt.sendToBugsnag(r,"error"),t}var Ot,Mt,Kt,Rt,Ht,Nt,Lt={};function Ut(t,n,i){var r;Lt[t]=null!==(r=Lt[t])&&void 0!==r?r:0,Lt[t]++,Lt[t]>1||Tt.sendToBugsnag(n,"error",i)}!function(t){t.MUT_INSERT=2,t.MUT_REMOVE=3,t.MUT_ATTR=4,t.MUT_TEXT=6,t.MOUSEMOVE=8,t.MOUSEMOVE_CURVE=9,t.SCROLL_LAYOUT=10,t.SCROLL_LAYOUT_CURVE=11,t.MOUSEDOWN=12,t.MOUSEUP=13,t.CLICK=16,t.FOCUS=17,t.VALUECHANGE=18,t.RESIZE_LAYOUT=19,t.DOMLOADED=20,t.LOAD=21,t.PLACEHOLDER_SIZE=22,t.UNLOAD=23,t.BLUR=24,t.SET_FRAME_BASE=25,t.TOUCHSTART=32,t.TOUCHEND=33,t.TOUCHCANCEL=34,t.TOUCHMOVE=35,t.TOUCHMOVE_CURVE=36,t.NAVIGATE=37,t.PLAY=38,t.PAUSE=39,t.RESIZE_VISUAL=40,t.RESIZE_VISUAL_CURVE=41,t.RESIZE_DOCUMENT_CONTENT=42,t.RESIZE_SCROLLABLE_ELEMENT_CONTENT=43,t.LOG=48,t.ERROR=49,t.DBL_CLICK=50,t.FORM_SUBMIT=51,t.WINDOW_FOCUS=52,t.WINDOW_BLUR=53,t.HEARTBEAT=54,t.WATCHED_ELEM=56,t.PERF_ENTRY=57,t.REC_FEAT_SUPPORTED=58,t.SELECT=59,t.CSSRULE_INSERT=60,t.CSSRULE_DELETE=61,t.FAIL_THROTTLED=62,t.AJAX_REQUEST=63,t.SCROLL_VISUAL_OFFSET=64,t.SCROLL_VISUAL_OFFSET_CURVE=65,t.MEDIA_QUERY_CHANGE=66,t.RESOURCE_TIMING_BUFFER_FULL=67,t.MUT_SHADOW=68,t.DISABLE_STYLESHEET=69,t.FULLSCREEN=70,t.FULLSCREEN_ERROR=71,t.ADOPTED_STYLESHEETS=72,t.CUSTOM_ELEMENT_DEFINED=73,t.MODAL_OPEN=74,t.MODAL_CLOSE=75,t.SLOW_INTERACTION=76,t.LONG_FRAME=77,t.TIMING=78,t.STORAGE_WRITE_FAILURE=79,t.DOCUMENT_PROPERTIES=80,t.ENTRY_NAVIGATE=81,t.STATS=82,t.VIEWPORT_INTERSECTION=83,t.COPY=84,t.PASTE=85,t.URL_SALT=86,t.URL_ID=87,t.FRAME_STATUS=88,t.SCRIPT_COMPILED_VERSION=89,t.RESET_CSS_SHEET=90,t.ANIMATION_CREATED=91,t.ANIMATION_METHOD_CALLED=92,t.ANIMATION_PROPERTY_SET=93,t.DOCUMENT_TIMELINE_CREATED=94,t.KEYFRAME_EFFECT_CREATED=95,t.KEYFRAME_EFFECT_METHOD_CALLED=96,t.KEYFRAME_EFFECT_PROPERTY_SET=97,t.CAPTURE_SOURCE=98,t.PAGE_DATA=99,t.VISIBILITY_STATE=100,t.DIALOG=101,t.CSSRULE_UPDATE=102,t.CANVAS=103,t.CANVAS_DETACHED_DIMENSION=104,t.INIT_API=105,t.DEFERRED_RESOLVED=106,t.KEEP_ELEMENT=2e3,t.KEEP_URL=2001,t.KEEP_BOUNCE=2002,t.SYS_SETVAR=8193,t.SYS_RESOURCEHASH=8195,t.SYS_SETCONSENT=8196,t.SYS_CUSTOM=8197,t.SYS_REPORTCONSENT=8198,t.SYS_LETHE_MOBILE_BUNDLE_SEQ=8199}(Ot||(Ot={})),function(t){t.Animation=0,t.CSSAnimation=1,t.CSSTransition=2}(Mt||(Mt={})),function(t){t.Unknown=0,t.Serialization=1}(Kt||(Kt={})),function(t){t.Unknown=0,t.Successful=1,t.BlocklistedFrame=2,t.PartiallyLoaded=3,t.MissingWindowOrDocument=4,t.MissingDocumentHead=5,t.MissingBodyOrChildren=6,t.AlreadyDefined=7,t.NoNonScriptElement=8,t.Exception=9}(Rt||(Rt={})),function(t){t.Unknown=0,t.DomSnapshot=1,t.NodeEncoding=2,t.LzEncoding=3}(Ht||(Ht={})),function(t){t.Internal=0,t.Public=1}(Nt||(Nt={}));var Ft,Dt,Bt,Wt,qt,Qt,Vt,zt,$t,Gt,Xt,Jt,Zt,Yt,tn,nn,rn,en,sn,on,un,an,cn,hn=["print","alert","confirm"];function fn(t){switch(t){case Ot.MOUSEDOWN:case Ot.MOUSEMOVE:case Ot.MOUSEMOVE_CURVE:case Ot.MOUSEUP:case Ot.TOUCHSTART:case Ot.TOUCHEND:case Ot.TOUCHMOVE:case Ot.TOUCHMOVE_CURVE:case Ot.TOUCHCANCEL:case Ot.CLICK:case Ot.SCROLL_LAYOUT:case Ot.SCROLL_LAYOUT_CURVE:case Ot.SCROLL_VISUAL_OFFSET:case Ot.SCROLL_VISUAL_OFFSET_CURVE:case Ot.NAVIGATE:return!0;}return!1}!function(t){t[t.Index=1]="Index",t[t.Cached=2]="Cached"}(Ft||(Ft={})),function(t){t.GrantConsent=!0,t.RevokeConsent=!1}(Dt||(Dt={})),function(t){t.Page=0,t.Document=1}(Bt||(Bt={})),function(t){t.Unknown=0,t.Api=1,t.FsShutdownFrame=2,t.Hibernation=3,t.Reidentify=4,t.SettingsBlocked=5,t.Size=6,t.Unload=7,t.Hidden=8}(Wt||(Wt={})),function(t){t.Unknown=0,t.NotEmpty=1,t.EmptyBody=2}(qt||(qt={})),function(t){t.Timing=0,t.Navigation=1,t.Resource=2,t.Paint=3,t.Mark=4,t.Measure=5,t.Memory=6,t.TimeOrigin=7,t.LayoutShift=8,t.FirstInput=9,t.LargestContentfulPaint=10,t.LongTask=11}(Qt||(Qt={})),function(t){t.Timing=["navigationStart","unloadEventStart","unloadEventEnd","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","domLoading","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd"],t.Navigation=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","unloadEventStart","unloadEventEnd","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd","type","redirectCount","decodedBodySize","encodedBodySize","transferSize"],t.Resource=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","decodedBodySize","encodedBodySize","transferSize"],t.Measure=["name","startTime","duration"],t.Memory=["jsHeapSizeLimit","totalJSHeapSize","usedJSHeapSize"],t.TimeOrigin=["timeOrigin"],t.LayoutShift=["startTime","value","hadRecentInput"],t.FirstInput=["name","startTime","duration","processingStart"],t.LargestContentfulPaint=["name","startTime","duration","renderTime","loadTime","size"]}(Vt||(Vt={})),function(t){t.Performance=0,t.PerformanceEntries=1,t.PerformanceMemory=2,t.Console=3,t.Ajax=4,t.PerformanceObserver=5,t.PerformanceTimeOrigin=7,t.WebAnimation=8,t.LayoutShift=9,t.FirstInput=10,t.LargestContentfulPaint=11,t.LongTask=12,t.HTMLDialogElement=13,t.CaptureOnStartEnabled=14,t.CanvasWatcherEnabled=15}(zt||(zt={})),function(t){t.Node=1,t.Sheet=2}($t||($t={})),function(t){t.StyleSheetHooks=0,t.SetPropertyHooks=1}(Gt||(Gt={})),function(t){t.Document="document",t.Event="evt",t.Page="page",t.User="user"}(Xt||(Xt={})),function(t){t.FsId="fsidentity",t.NewUid="newuid"}(Jt||(Jt={})),function(t){t.Elide=0,t.Record=1,t.Allowlist=2}(Zt||(Zt={})),function(t){t.Any=0,t.Exclude=1,t.Mask=2}(Yt||(Yt={})),function(t){t.Erase=0,t.MaskText=1,t.ScrubUrl=2,t.ScrubCss=3}(tn||(tn={})),function(t){t.Static=0,t.Prefix=1}(nn||(nn={})),function(t){t.SignalInvalid=0,t.SignalDeadClick=1,t.SignalRageClick=2}(rn||(rn={})),function(t){t.ReasonNoSuchOrg=1,t.ReasonOrgDisabled=2,t.ReasonOrgOverQuota=3,t.ReasonBlockedDomain=4,t.ReasonBlockedIp=5,t.ReasonBlockedUserAgent=6,t.ReasonBlockedGeo=7,t.ReasonBlockedTrafficRamping=8,t.ReasonInvalidURL=9,t.ReasonUserOptOut=10,t.ReasonInvalidRecScript=11,t.ReasonDeletingUser=12,t.ReasonNativeHookFailure=13}(en||(en={})),function(t){t.Unset=0,t.Exclude=1,t.Mask=2,t.Unmask=3,t.Watch=4,t.Keep=5,t.Defer=6}(sn||(sn={})),function(t){t.Unset=0,t.Click=1}(on||(on={})),function(t){t[t.Page=1]="Page",t[t.Bundle=2]="Bundle"}(un||(un={})),function(t){t[t.Error=3]="Error",t[t.Page=4]="Page",t[t.Bundle=5]="Bundle",t[t.Settings=6]="Settings"}(an||(an={})),function(t){t.MaxPerfMarksPerPage=16384,t.MaxLogsPerPage=1024,t.MaxUrlLength=2048,t.MutationProcessingInterval=250,t.CurveSamplingInterval=142,t.DefaultBundleUploadInterval=5e3,t.HeartbeatInitial=4e3,t.HeartbeatMax=256200,t.PageInactivityTimeout=18e5,t.BackoffMax=3e5,t.ScrollSampleInterval=t.MutationProcessingInterval/5,t.InactivityThreshold=4e3,t.MaxAjaxPayloadLength=16384,t.DefaultOrgSettings={MaxPerfMarksPerPage:t.MaxPerfMarksPerPage,MaxConsoleLogPerPage:t.MaxLogsPerPage,MaxAjaxPayloadLength:t.MaxAjaxPayloadLength,MaxUrlLength:t.MaxUrlLength,RecordPerformanceResourceImg:!0,RecordPerformanceResourceTiming:!0,HttpRequestHeadersAllowlist:[],HttpResponseHeadersAllowlist:[],UrlPrivacyConfig:[{Exclude:{Hash:[{Expression:"#.*"}],QueryParam:[{Expression:"(=)(.*)"}]}}],AttributeBlocklist:[{Target:Yt.Any,Tag:"*",Name:"",Type:nn.Prefix,Action:tn.Erase}]},t.DefaultStatsSettings={MaxPayloadLength:8192,MaxEventTypeLength:1024},t.BlockedFieldValue="__fs__redacted"}(cn||(cn={}));var vn,ln,dn,pn="_fs_uid",wn="_fs_cid",gn="_fs_lua";function mn(t,n,i,r){void 0!==i&&("function"==typeof t.addEventListener?t.addEventListener(n,i,r):"function"==typeof t.addListener&&t.addListener(i))}function yn(t,n,i,r){void 0!==i&&("function"==typeof t.removeEventListener?t.removeEventListener(n,i,r):"function"==typeof t.removeListener&&t.removeListener(i))}!function(t){t[t.Shutdown=1]="Shutdown",t[t.Starting=2]="Starting",t[t.Started=3]="Started"}(vn||(vn={})),function(t){t.Set=0,t.Function=1}(ln||(ln={})),function(t){t[t.Disabled=0]="Disabled",t[t.CaptureCanvasOps=1]="CaptureCanvasOps",t[t.ScreenshotCanvas=2]="ScreenshotCanvas"}(dn||(dn={}));var bn=function(){function t(){var t=this;this.T=[],this.C=[],this.P=!0,this.j=!1;try{var n=Object.defineProperty({},"passive",{get:function(){t.P={capture:!0,passive:!0},t.j={capture:!1,passive:!0}}});window.addEventListener("test",J,n)}catch(t){}}return t.prototype.add=function(t,n,i,r,e){return void 0===e&&(e=!1),this.addCustom(t,n,i,r,e)},t.prototype.addCustom=function(t,n,i,r,e){void 0===e&&(e=!1);var s={target:t,type:n,fn:Tt.wrap(function(t){(e||!1!==t.isTrusted||"message"==n||t._fs_trust_event)&&r(t)}),options:i?this.P:this.j,index:this.T.length};return this.T.push(s),mn(t,n,s.fn,s.options),s},t.prototype.remove=function(t){t.target&&(yn(t.target,t.type,t.fn,t.options),t.target=null,t.fn=void 0)},t.prototype.clear=function(){for(var t=0;ti){n.Z||(n.Z=!0,Tt.sendToBugsnag("Out of time for remaining measurement tasks.","warning",{totalRunningTimeMs:a-t}));break t}}n.G=null}finally{n.X=!1,n.wnd}}}),this.wnd=t}return t.create=function(t){return t.ResizeObserver?new ai(t,t.ResizeObserver):new ci(t)},t.prototype.requestMeasureTask=function(t,n){var i,r=this;if(this.J>16)Tt.sendToBugsnag("Too much synchronous recursion in requestMeasureTask","error");else{var e=this.X?this.J:0,s=Tt.wrap(function(){var t=r.J;r.J=e+1;try{n()}finally{r.J=t}});this.G?this.G[t].push(s):(this.G=((i={})[ii.Essential]=[],i[ii.High]=[],i[ii.Medium]=[],i[ii.Low]=[],i[t]=[s],i),this.schedule())}},t.prototype.performMeasurementsNow=function(){this.performMeasurements()},t}(),ai=function(t){function n(n,i){var r=t.call(this,n)||this;return r.Y=i,r}return(0,e.__extends)(n,t),n.prototype.schedule=function(){var t=this,n=this.Y,i=this.wnd.document,r=i.documentElement||i.body||i.head,e=new n(function(){e.unobserve(r),t.performMeasurements()});e.observe(r)},n}(ui),ci=function(t){function n(n){return t.call(this,n)||this}return(0,e.__extends)(n,t),n.prototype.schedule=function(){(0,e.__awaiter)(void 0,void 0,Yn,function(){var t;return(0,e.__generator)(this,function(n){switch(n.label){case 0:return(t=o.requestWindowAnimationFrame)?[4,new Yn(function(n){return t(window,n)})]:[3,2];case 1:n.sent(),n.label=2;case 2:return[4,ei()];case 3:return n.sent(),[2];}})}).then(this.performMeasurements)},n}(ui);function hi(t,n){return n&&t.pageLeft==n.pageLeft&&t.pageTop==n.pageTop}function fi(t,n){return n&&t.width==n.width&&t.height==n.height}function vi(t){return{pageLeft:t.pageLeft,pageTop:t.pageTop,width:t.width,height:t.height}}var li=[["@import\\s+\"","\""],["@import\\s+'","'"]].concat([["url\\(\\s*\"","\"\\s*\\)"],["url\\(\\s*'","'\\s*\\)"],["url\\(\\s*","\\s*\\)"]]),di=".*?"+/(?:[^\\](?:\\\\)*)/.source,pi=new RegExp(li.map(function(t){var n=t[0],i=t[1];return"("+n+")("+di+")("+i+")"}).join("|"),"g"),wi=/url\(["']?(.+?)["']?\)/g,gi=/^\s*\/\//;function mi(t){return"BackCompat"==t.compatMode}function yi(t){return t&&t.body&&t.documentElement?mi(t)?[t.body.clientWidth,t.body.clientHeight]:[t.documentElement.clientWidth,t.documentElement.clientHeight]:[0,0]}var bi=function(){function t(t,n){var i,r,e,s;this.hasKnownPosition=!1,this.pageLeft=0,this.pageTop=0,this.width=0,this.height=0,this.clientWidth=0,this.clientHeight=0;var o=t.document;if(o&&o.documentElement&&o.body){i=yi(o),this.clientWidth=i[0],this.clientHeight=i[1];var u=t.visualViewport;if(u){this.hasKnownPosition=!0,this.pageTop=u.pageTop-u.offsetTop,this.pageLeft=u.pageLeft-u.offsetLeft,0==this.pageTop&&(this.pageTop=0),0==this.pageLeft&&(this.pageLeft=0);var a=null!==(e=xi(t,"innerWidth"))&&void 0!==e?e:0,c=null!==(s=xi(t,"innerHeight"))&&void 0!==s?s:0;if(a>0&&c>0)return this.width=a,void(this.height=c)}if(void 0!==n&&this.clientWidth==n.clientWidth&&this.clientHeight==n.clientHeight&&n.width>0&&n.height>0)return this.width=n.width,void(this.height=n.height);r=this.tt(t),this.width=r[0],this.height=r[1]}}return t.prototype.tt=function(t){var n=this.it(t,"width",this.clientWidth,this.clientWidth+128);void 0===n&&(n=xi(t,"innerWidth")),void 0===n&&(n=this.clientWidth);var i=this.it(t,"height",this.clientHeight,this.clientHeight+128);return void 0===i&&(i=xi(t,"innerHeight")),void 0===i&&(i=this.clientHeight),[n,i]},t.prototype.it=function(t,n,i,r){if(o.matchMedia){var e=i,s=r,u=o.matchMedia(t,"(min-"+n+": "+e+"px)");if(null!=u){if(u.matches&&o.matchMedia(t,"(max-"+n+": "+e+"px)").matches)return e;for(;e<=s;){var a=o.mathFloor((e+s)/2);if(o.matchMedia(t,"(min-"+n+": "+a+"px)").matches){if(o.matchMedia(t,"(max-"+n+": "+a+"px)").matches)return a;e=a+1}else s=a-1}}}},t}();function Ei(t,n){return new bi(t,n)}var Si=function(t,n){this.offsetLeft=0,this.offsetTop=0,this.pageLeft=0,this.pageTop=0,this.width=0,this.height=0,this.scale=0;var i=t.document;if(i.body){"pageXOffset"in t?(this.pageLeft=t.pageXOffset,this.pageTop=t.pageYOffset):i.scrollingElement?(this.pageLeft=i.scrollingElement.scrollLeft,this.pageTop=i.scrollingElement.scrollTop):mi(i)?(this.pageLeft=i.body.scrollLeft,this.pageTop=i.body.scrollTop):i.documentElement&&(i.documentElement.scrollLeft>0||i.documentElement.scrollTop>0)?(this.pageLeft=i.documentElement.scrollLeft,this.pageTop=i.documentElement.scrollTop):(this.pageLeft=i.body.scrollLeft||0,this.pageTop=i.body.scrollTop||0),this.offsetLeft=this.pageLeft-n.pageLeft,this.offsetTop=this.pageTop-n.pageTop;var r=0,e=0;try{r=t.innerWidth,e=t.innerHeight}catch(t){return}if(0!=r&&0!=e){this.scale=n.width/r,this.scale<1&&(this.scale=1);var s=n.width-n.clientWidth,o=n.height-n.clientHeight;this.width=r-s/this.scale,this.height=e-o/this.scale}}};function xi(t,n){try{return t[n]}catch(t){return}}function ki(t){var n=t;return n.tagName?"object"==typeof n.tagName?"form":n.tagName.toLowerCase():null}var _i,Ai,Ii=new RegExp("[^\\s]"),Ti=new RegExp("[\\s]*$");function Ci(t){var n=Ii.exec(t);if(!n)return t;for(var i=n.index,r=(n=Ti.exec(t))?t.length-n.index:0,e="\uFFFF",s=t.slice(i,t.length-r).split(/\r\n?|\n/g),o=0;o0&&n.length<1e4;){var i=n.pop();delete Mi[i.id],i.node._fs==i.id&&(i.node._fs=0),i.id=0,i.next&&n.push(i.next),i.child&&n.push(i.child)}jt(n.length<1e4,"clearIds is fast")}function Qi(t,n){void 0===n&&(n=1024);try{var i={tokens:[],opath:[],cyclic:Vi(t,n/4)};return $i(t,n,0,i),i.tokens.join("")}catch(t){return lt(t)}}function Vi(t,n){var i=0;try{o.jsonStringify(t,function(t,r){if(i++>n)throw"break";if("object"==typeof r)return r})}catch(t){return"break"!=t}return!1}var zi=function(t,n,i){return void 0===i&&(i="..."),t.length<=n?t:t.length<=i.length||n<=i.length?t.substring(0,n):t.substring(0,n-i.length)+i};function $i(t,n,i,r){if(n<1)return 0;var e=function(t){switch(!0){case function(t){return!(!t||t.constructor!=Date)}(t):return n=t,isNaN(n)?"Invalid Date":n.toUTCString();case function(t){return"object"==typeof Node?t instanceof Node:t&&"object"==typeof t&&t.nodeType>0&&"string"==typeof t.nodeName}(t):return function(t){return t.toString()}(t);case void 0===t:return"undefined";case"object"!=typeof t||null==t:return t;case t instanceof Error:return[t.toString(),t.stack].filter(Boolean).join(",");}var n}(t);if(void 0!==e){var s=function(t,n){var i=o.jsonStringify(t);return i&&"\""==i[0]?zi(i,n,"...\""):i}(e,n);return"string"==typeof s&&s.length<=n?(r.tokens.push(s),s.length):0}if(r.cyclic){r.opath.splice(i);var u=r.opath.lastIndexOf(t);if(u>-1){var a="";return a="\""+zi(a,n-2)+"\"",r.tokens.push(a),a.length}r.opath.push(t)}var c=n,h=function(t){return c>=t.length&&(c-=t.length,r.tokens.push(t),!0)},f=function(t){var n=r.tokens.length-1;","===r.tokens[n]?r.tokens[n]=t:h(t)};if(c<2)return 0;if(tt(t)){h("[");for(var v=0;v0;v++){var l=$i(t[v],c-1,i+1,r);if(c-=l,0==l&&!h("null"))break;h(",")}f("]")}else{h("{");var d=nt(t);for(v=0;v0;v++){var p=d[v],w=t[p];if(!h("\""+p+"\":"))break;if(0==(l=$i(w,c-1,i+1,r))){r.tokens.pop();break}c-=l,h(",")}f("}")}return n==1/0?1:n-c}var Gi,Xi,Ji=function(){function t(){var n=this;this.rt=Tt.wrap(function(){n.unregister(),n.et&&n.et()}),this.st=0,this.ot=t.ut++}return t.ct=function(){t.checkedAlready=!1,t.ht=0},t.checkForBrokenSchedulers=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){var n,i;return(0,e.__generator)(this,function(r){switch(r.label){case 0:return!o.requestWindowAnimationFrame||t.checkedAlready||(n=p())-t.ht<100?[2,!1]:(t.ht=n,t.checkedAlready=!0,[4,new Yn(function(t){return o.requestWindowAnimationFrame(window,t)})]);case 1:return r.sent(),i=[],rt(t.ft,function(t){var r=t.vt(n);r&&i.push(r)}),[4,Yn.all(i)];case 2:return r.sent(),o.requestWindowAnimationFrame(window,Tt.wrap(function(){t.checkedAlready=!1})),[2,!0];}})})},t.stopAll=function(){rt(this.ft,function(t){return t.stop()})},t.prototype.setTick=function(t){this.et=t},t.prototype.stop=function(){this.cancel(),delete t.ft[this.ot]},t.prototype.register=function(n){this.st=p()+100+1.5*n,t.ft[this.ot]=this},t.prototype.timerIsRunning=function(){return null!=t.ft[this.ot]},t.prototype.unregister=function(){delete t.ft[this.ot]},t.prototype.vt=function(t){if(t>this.st)return Yn.resolve().then(this.rt)["catch"](function(){})},t.ft={},t.ut=0,t.checkedAlready=!1,t.ht=0,t}(),Zi=function(t){function n(n){var i=t.call(this)||this;return i.lt=n,i.dt=-1,i}return(0,e.__extends)(n,t),n.prototype.start=function(t){var n=this;-1==this.dt&&(this.setTick(function(){t(),n.register(n.lt)}),this.dt=o.setWindowInterval(window,this.rt,this.lt),this.register(this.lt))},n.prototype.cancel=function(){-1!=this.dt&&(o.clearWindowInterval(window,this.dt),this.dt=-1,this.setTick(function(){}))},n}(Ji),Yi=function(t){function n(n,i,r){void 0===i&&(i=0);for(var e=[],s=3;sn&&(this.St=t-n,this.St>1e3&&this.kt("timekeeper set with future ts"))},t.prototype.kt=function(t){Qi({msg:t,skew:this.St,startTime:this.xt,wallTime:this.wallTime()},1024)},t}(),ir=function(){function t(t,n){this._t=t,this.At=n,this.It=!1,this.Tt={},this.Ct={},this.Pt={},this.jt=!1,this.Ot=!1,Gi=this,this.Mt=t.window.document}return t.prototype.start=function(){var t;(t=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value"))&&t.set&&(rr||(yt(HTMLInputElement,"value",ar),yt(HTMLInputElement,"checked",ar),yt(HTMLSelectElement,"value",ar),yt(HTMLTextAreaElement,"value",ar),yt(HTMLSelectElement,"selectedIndex",ar),yt(HTMLOptionElement,"selected",ar),rr=!0),1)||(this.It=!0)},t.prototype.hookInstance=function(t){if("input"===ki(t))switch(t.type){case"checkbox":case"radio":bt(t,"checked",ar);break;default:bt(t,"value",ar);}},t.prototype.addInput=function(t){if(t){var n=Bi(t);if(n){"input"===ki(t)&&this.Kt(t);var i=!1;if(function(t){switch(t.type){case"checkbox":case"radio":return t.checked!=t.hasAttribute("checked");default:return(t.value||"")!=function(t){if("select"!=ki(t))return t.getAttribute("value")||"";var n=t,i=n.querySelector("option[selected]")||n.querySelector("option");return i&&i.value||""}(t);}}(t)&&(this.Rt(t),i=!0),this.It&&(this.Tt[n]={elem:t}),!i)if(hr(t)){var r=or(t);t.checked&&(this.Pt[r]=n)}else this.Ct[n]=cr(t)}}},t.prototype.Kt=function(t){if(this.jt)this.Ot&&this.hookInstance(t);else{var n="checkbox"===t.type||"radio"===t.type?"checked":"value",i=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,n),r=Object.getOwnPropertyDescriptor(t,n);i&&r&&i!==r&&(this.Ot=!0,this.hookInstance(t)),this.jt=!0}},t.prototype.diffValue=function(t,n){void 0===n&&(n=cr(t));var i=Bi(t);if(!t||!i)return!1;if(hr(t)){var r=or(t);return this.Pt[r]===i!=("true"===n)}return this.Ct[i]!==n},t.prototype.onChange=function(t,n,i){void 0===i&&(i=cr(t));var r=Bi(t);t&&r&&(n||this.diffValue(t,i))&&this.Rt(t,n)},t.prototype.onKeyboardChange=function(t){var n,i=function(t){for(var n=t.activeElement;n&&n.shadowRoot;){var i=n.shadowRoot.activeElement;if(!i)return n;n=i}return n}(this.Mt);i&&("value"in(n=i)||"checked"in n)&&!Hi(i)&&this.diffValue(i)&&this.Rt(i,t)},t.prototype.tick=function(){for(var t in this.Tt){var n=this.Tt[t],i=n.elem;if(Bi(i))try{delete this.Tt[t];var r=cr(i);if(this.diffValue(i,r))this.Rt(i);else if(n.noFsIdInOption){var e=i;Array.prototype.slice.call(e.options).every(function(t){return Bi(t)})&&(n.noFsIdInOption=!1,this.Rt(i))}}finally{this.It&&(this.Tt[t]=n)}else delete this.Tt[t],delete this.Ct[t],hr(i)&&delete this.Pt[or(i)]}},t.prototype.stop=function(){Gi=void 0},t.prototype.Rt=function(t,n){var i=this;void 0===n&&(n=!1);var r=Bi(t);if(t&&r&&!this.Ht(r,t)){var e=cr(t);if(hr(t)){var s=or(t);"false"===e&&this.Pt[s]===r?delete this.Pt[s]:"true"===e&&(this.Pt[s]=r)}else this.Ct[r]=e;this._t.measurer.requestMeasureTask(ii.Medium,function(){var s=t.getBoundingClientRect(),o=s.width>0&&s.height>0,u=Ni(t)?Ci(e):e;i.At.enqueue({Kind:Ot.VALUECHANGE,Args:[r,u,n,o]})})}},t.prototype.Ht=function(t,n){if(this.Tt[t])return!0;if("select"!==ki(n))return!1;for(var i=n.options,r=0;r-1||wr.indexOf("Trident/")>-1,mr=(gr&&wr.indexOf("Trident/5"),gr&&wr.indexOf("Trident/6"),gr&&wr.indexOf("rv:11")>-1),yr=wr.indexOf("Edge/")>-1,br=(wr.indexOf("CriOS"),wr.indexOf("Snapchat")>-1),Er=/^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);function Sr(){var t=window.navigator.userAgent.match(/Version\/(\d+)/);return t&&t[1]?parseInt(t[1],10):-1}function xr(t){if(!Er)return!1;var n=Sr();return n>=0&&n===t}function kr(t){if(!Er)return!1;var n=Sr();return n>=0&&nne?(Tt.sendToBugsnag("Ignoring huge text node","warning",{length:s}),""):t.parentNode&&"style"==ki(t.parentNode)?r:e.mask?Ci(r):r}function re(t){return Kr[t]||t.toLowerCase()}var ee=/^\s*((prefetch|preload|prerender)\s*)+$/i,se=/^\s*.*((worklet|script|worker|font|fetch)\s*)+$/i;function oe(t,n,i,r,e){var s,u;if(void 0===r&&(r=ki(t)),void 0===e&&(e=Ui(t)),null===r||""===n)return null;if("link"===r&&ee.test(null!==(s=t.getAttribute("rel"))&&void 0!==s?s:"")&&!se.test(null!==(u=t.getAttribute("as"))&&void 0!==u?u:""))return null;var a,c="style"===n?ae(i):i,h=function(t,n,i){var r,e,s,u,a,c,h,f,v,l,d,p,w,g=void 0;(null===(r=null==n?void 0:n.watchKind)||void 0===r?void 0:r.has(_i.Exclude))?g=Yt.Exclude:(null==n?void 0:n.mask)&&(g=Yt.Mask);var m=[null===(u=null===(s=null===(e=Ee.blocklist[Yt.Any])||void 0===e?void 0:e[t])||void 0===s?void 0:s["static"])||void 0===u?void 0:u[i],null===(h=null===(c=null===(a=Ee.blocklist[Yt.Any])||void 0===a?void 0:a["*"])||void 0===c?void 0:c["static"])||void 0===h?void 0:h[i],g?null===(l=null===(v=null===(f=Ee.blocklist[g])||void 0===f?void 0:f[t])||void 0===v?void 0:v["static"])||void 0===l?void 0:l[i]:void 0,g?null===(w=null===(p=null===(d=Ee.blocklist[g])||void 0===d?void 0:d["*"])||void 0===p?void 0:p["static"])||void 0===w?void 0:w[i]:void 0];return Ee.hasPrefix&&m.push(ke(Yt.Any,t,i),ke(Yt.Any,"*",i),g?ke(g,t,i):void 0,g?ke(g,"*",i):void 0),function(t){var n=t.filter(function(t){return void 0!==t});if(0!==n.length)return o.mathMin.apply(o,n)}(m)}(r,e,n);if(void 0===h&&!e)return null;switch(h){case void 0:return c;case tn.Erase:return null;case tn.MaskText:return Ci(c);case tn.ScrubCss:return a=function(t,n,i){return""+t+Se+i},c.replace(pi,function(t){for(var n=[],i=1;i-1)return f.substring(v)}return f;default:return(0,Ir.nt)(h);}}var ue={},ae=function(t,n){void 0===n&&(n=window);try{var i=n.location,r=""+i.origin+i.pathname+i.search,e=ue[r];return e?e.lastIndex=0:(e=new RegExp((s=r,($r.test(s)?s.replace(zr,"\\$&"):s)+"/?(#)"),"g"),ue[r]=e),t.replace(e,"https://fs-currenturl.invalid$1")}catch(n){return Ut("cleanCSS",n),t}var s},ce=/^data:/i;function he(t,n){if(ce.test(t))return t;switch(n.source){case"dom":switch(i=n.type){case"frame":case"iframe":return we(t);default:return fe(t);}case"event":switch(i=n.type){case Ot.AJAX_REQUEST:case Ot.NAVIGATE:return fe(t);case Ot.SET_FRAME_BASE:return we(t);default:return(0,Ir.nt)(i);}case"log":return we(t);case"page":var i;switch(i=n.type){case"base":return we(t);case"referrer":case"url":return fe(t);default:return(0,Ir.nt)(i);}case"perfEntry":switch(n.type){case"frame":case"iframe":case"navigation":case"other":return we(t);default:return fe(t);}default:return(0,Ir.nt)(n);}}function fe(t){return ge(de,t)}var ve=cn.DefaultOrgSettings.MaxUrlLength,le=Rr(cn.DefaultOrgSettings.UrlPrivacyConfig),de=Rr(cn.DefaultOrgSettings.UrlPrivacyConfig);function pe(t,n){le=Rr(cn.DefaultOrgSettings.UrlPrivacyConfig.concat(t)),de=Rr(t),ve=n||cn.DefaultOrgSettings.MaxUrlLength}function we(t){return ge(le,t)}function ge(t,n){return function(t,n,i){void 0===i&&(i=Lr);for(var r={Hash:[],Host:[],Path:[],QueryParam:[],Query:[]},e=0,s=t;e").replace(ye,function(t){return he(t,{source:"log",type:"debug"})})}var Ee,Se="https://fs-excluded.invalid";function xe(t){var n,i,r,e,s,o,u,a,c,h,f,v,l,d,p,w;try{for(var g=(Ee={blocklist:{},hasPrefix:!1}).blocklist,m=(null!==(r=null==t?void 0:t.length)&&void 0!==r?r:0)>0?t:cn.DefaultOrgSettings.AttributeBlocklist,y={},b=0,E=m;b-1;var n}var Te="#polyfillshadow";function Ce(t){var n;return(null===(n=t.childNodes)||void 0===n?void 0:n.length)>0}function Pe(t,n){Oe(t.childNodes,n)}function je(t,n){Oe(t.childNodes,n,!0)}function Oe(t,n,i){void 0===i&&(i=!1);for(var r=i?t.length-1:0,e=i?-1:t.length;r!==e;){var s=t[r];s&&"frag"in s&&!St(s)&&Array.isArray(s.frag)?s.frag.length&&Oe(s.childNodes,n,i):n(s),i?--r:++r}}var Me={INPUT:!0,TEXTAREA:!0,NOSCRIPT:!0},Ke=function(){function t(t,n,i){this.Xt=t,this.Jt=n,this.Zt=i,Mi={},Ki=1}return t.prototype.tokenizeNode=function(t,n,i,r,e,s,o){var u=this,a=Ui(n),c=Ui(i),h=[];return function(n){var i=Ki;try{return u.Yt(t,a,c,r,h,e,s,o),!0}catch(t){return Ki=i,!1}}()||(h=[]),h},t.prototype.Yt=function(t,n,i,r,s,o,u,a){for(var c,h,f=[{parentMirror:n,nextMirror:i,node:r}],v=function(t,n){return function(i){i&&t.push({parentMirror:n,nextMirror:null,node:i})}};f.length;){var l=f.pop();if(l)if("string"!=typeof l){var d=l.node,p=ki(d),w=this.tn(t,p,l,s,o,u);if(null!=w&&!(null===(c=w.watchKind)||void 0===c?void 0:c.has(_i.Exclude))){var g=1===d.nodeType?d.shadowRoot:null,m=w.shadowRootType===Te&&window.HTMLSlotElement&&"slot"===p&&d.assignedNodes();if(g||m||Ce(d))if(null===(h=w.watchKind)||void 0===h?void 0:h.has(_i.Defer))a(w.node,Ai.Deferred);else{if(f.push("]"),je(d,v(f,w)),g)f.push({parentMirror:w,nextMirror:null,node:g});else if(m&&m.length>0){for(var y=[],b=!1,E=0,S=m;E1e3)return null;if(!i||1!=i.nodeType)return null;var r=i;if(getComputedStyle(r).display.indexOf("inline")<0)return r;i=i.parentNode}},n}(Ze),ts=function(t){function n(){return null!==t&&t.apply(this,arguments)||this}return(0,e.__extends)(n,t),n.prototype.observe=function(t){var n=this;if(t&&1==t.nodeType){var i=t;this.Tn(Ui(t)),this._t.measurer.requestMeasureTask(ii.Medium,function(){n.addEntry(i)})}},n.prototype.unobserveSubtree=function(t){var n=Ui(t);n&&this.Cn(n)},n.prototype.nodeChanged=function(t){var n=this,i=this.Pn(t);this._t.measurer.requestMeasureTask(ii.Medium,function(){for(var t=0,r=i;t0||this.Hn.length>0){var r={},s={};for(var o in this.Gn(t,i,s,r),s){var u=o.split("\t");i.push({Kind:Ot.MUT_ATTR,Args:[parseInt(u[0],10),u[1],s[o]],When:t})}for(var o in r)i.push({Kind:Ot.MUT_TEXT,Args:[parseInt(o,10),r[o]],When:t})}var a=this.Rn;this.Rn=[];for(var c=0;c0&&(i.push({Kind:Ot.DEFERRED_RESOLVED,Args:(0,e.__spreadArray)([],this.Ln),When:t}),this.Ln=[]),this.Nn.length>0){for(var f=0,v=this.Nn;f0&&this.Un.push(es(l))}this.Nn=[]}return i},t.prototype.recordingIsDetached=function(){return!!this.Wn&&this.Wn!=this.Dn.document},t.prototype.$n=function(t,n){if(!this.Kn&&this.Wn){window;var i=this.Xt.allWatchedElements(this.Wn);this.Zn(i,t,n,null,this.Wn,null),this.Jt.nodeChanged(this.Wn),this.qn&&this.Xn(this.Wn),this.Kn=!0,this.Yn(),window}},t.prototype.Yn=function(){var t=this;this.zn=mt(Element.prototype,"attachShadow",!0),this.zn&&this.zn.before(function(n){n.that.shadowRoot||t.Rn.push(n.that)})},t.prototype.Xn=function(t){var n;try{null===(n=this.qn)||void 0===n||n.observe(t,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0})}catch(t){}},t.prototype.Gn=function(t,n,i,r){for(var e,s,o,u,a=this,c={},h={},f=function(i){if(Ui(i)){a.ti(t,n,Ui(i));var r=Ui(i.parentNode);r&&(h[r.id]=r.node)}},v=0;v0)for(var m=0;m0){h[g]=l.target;var y=!(null==(T=l.target)?void 0:T.shadowRoot)||Ie(T.shadowRoot)?null:Ui(T.shadowRoot);y&&(h[y.id]=y.node)}break;case"characterData":Hi(l.target)||l.oldValue!=l.target.textContent&&(r[g]=ie(l.target));break;case"attributes":var b=ki(j=l.target);if("link"===b&&"rel"===l.attributeName&&ee.test(null!==(o=l.oldValue)&&void 0!==o?o:"")){f(j);break}var E,S=Li(j),x=this.Xt.isWatched(j);if($e(x)>$e(S)){f(j);break}De.needsToObserve(S,x)&&(this.Jt.observe(j),(null==x?void 0:x.has(_i.Watch))&&(null===(u=this.Zt)||void 0===u||u.observe(j)),(E=Ui(j))&&(E.watchKind=De.combineKindsPreservePrivacy(S,x)));var k=(void 0===(I=l.attributeNamespace)&&(I=""),(null===I?"":{"http://www.w3.org/1999/xlink":"xlink:","http://www.w3.org/XML/1998/namespace":"xml:","http://www.w3.org/2000/xmlns/":"xmlns:"}[I]||"")+(l.attributeName||"")),_=re(k);if("dialog"===b&&"open"===k)break;if(j.hasAttribute(k)){var A=l.target.getAttribute(k);l.oldValue!=A&&(A=oe(l.target,_,A||"",b),this.Mn(b,l.target,((e={})[_]=A||"",e)),null!==A&&(i[g+"\t"+_]=A))}else i[g+"\t"+_]=null;}}catch(t){}for(var I,T,C=0,P=this.Hn;C0&&i.push({Kind:Ot.MUT_SHADOW,Args:[s,u],When:n},{Kind:Ot.TIMING,Args:[[Nt.Internal,Kt.Serialization,Ht.NodeEncoding,n,a]],When:n})},t.prototype.Zn=function(t,n,i,r,e,s){var o=Di(r)||-1,u=Di(s)||-1,a=-1===o&&-1===u,c=p();window;var h=this.ei(t,r,e,s);window;var f=p()-c;h.length>0&&i.push({Kind:Ot.MUT_INSERT,Args:[o,u,h],When:n},{Kind:Ot.TIMING,Args:[[Nt.Internal,Kt.Serialization,a?Ht.DomSnapshot:Ht.NodeEncoding,n,f]],When:n})},t.prototype.ei=function(t,n,i,r){var e=this;if(n&&Hi(n))return[];for(var s=[],o=this.Bn.tokenizeNode(t,n,r,i,function(t){if(1==t.nodeType){var n=t;if(n.shadowRoot&&e.Xn(n.shadowRoot),"SLOT"===t.nodeName){var i=Ui(t);(null==i?void 0:i.shadowRootType)===Te&&t.addEventListener("slotchange",Tt.wrap(function(n){var i;e.Hn.push(null!==(i=n.target)&&void 0!==i?i:t)}))}}e.jn(t,s)},this.Mn,function(t,n){switch(n){case Ai.Immediate:e.refreshElement(t);break;case Ai.Deferred:e.Nn.push(t);}}),u=0,a=s;u0){var e=n[n.length-1];if(e.Kind==Ot.MUT_REMOVE)return void e.Args.push(r)}n.push({Kind:Ot.MUT_REMOVE,Args:[r],When:t})},t.prototype.setUpIEWorkarounds=function(){var n=this;if(mr){var i=Object.getOwnPropertyDescriptor(Node.prototype,"textContent"),r=i&&i.set;if(!i||!r)throw new Error("Missing textContent setter -- not safe to record mutations.");Object.defineProperty(Element.prototype,"textContent",(0,e.__assign)((0,e.__assign)({},i),{set:function(t){try{for(var n=void 0;n=this.firstChild;)this.removeChild(n);if(null===t||""==t)return;var i=(this.ownerDocument||document).createTextNode(t);this.appendChild(i)}catch(n){r&&r.call(this,t)}}}))}this.si=new tr(t.ThrottleMax,t.ThrottleInterval,function(){return new Yi(function(){n.Fn=!0,n.tearDownIEWorkarounds()}).start()});var s=this.si.guard(function(t){var n=t.cssText;t.cssText=n});this.si.open(),this.oi=mt(CSSStyleDeclaration.prototype,"setProperty"),this.oi&&this.oi.afterSync(function(t){s(t.that)}),this.ui=mt(CSSStyleDeclaration.prototype,"removeProperty"),this.ui&&this.ui.afterSync(function(t){s(t.that)})},t.prototype.tearDownIEWorkarounds=function(){this.si&&this.si.close(),this.oi&&this.oi.disable(),this.ui&&this.ui.disable()},t.prototype.updateConsent=function(){var t=this;this.Wn&&Pe(this.Wn,function(n){return t.refreshElement(n)})},t.prototype.refreshElement=function(t){Di(t)&&this.Hn.push(t)},t.ThrottleMax=1024,t.ThrottleInterval=1e4,t}();function os(t){for(var n=new WeakMap,i=t;i;i=i.parentNode){if(n.has(i))return null;if(n.set(i,!0),11===i.nodeType)break}if(!i)return null;var r=Ui(i);return(null==r?void 0:r.shadowRootType)===Te&&(null==r?void 0:r.parent)?[r.parent.id,r.parent.node]:null}var us="navigation",as="resource",cs="paint",hs="measure",fs="mark",vs="layout-shift",ls="first-input",ds="largest-contentful-paint",ps="longtask",ws=function(){function t(t,n,i,r){var e=this;this._t=t,this.At=n,this.ai=r,this.ci=!1,this.hi=!1,this.fi=!1,this.vi=!1,this.li=!1,this.di=!1,this.pi=!1,this.wi=cn.DefaultOrgSettings.MaxPerfMarksPerPage,this.gi=0,this.mi=!1,this.yi=!1,this.bi=!1,this.Ei=!1,this.Si=0,this.xi=!1,this.qn=null,this.ki=[],this._i=new Yn(function(t){e.Ai=function(){t({timeRemaining:function(){return Number.POSITIVE_INFINITY},didTimeout:!1}),e.Ai=void 0}}),this.Ii=!1;var s=window.performance;s&&(this.fi=!0,s.timing&&(this.vi=!0),s.memory&&(this.di=!0),s.timeOrigin&&(this.pi=!0),"function"==typeof s.getEntries&&(this.li=!0),this.mi=gs(window,vs),this.yi=gs(window,ls),this.bi=gs(window,ds),this.Ei=gs(window,ps),this.T=i.createChild())}return t.prototype.initialize=function(t){var n=t.resourceUploader,i=t.recTimings,r=t.recImgs,e=t.maxPerfMarksPerPage;this.Ti=n,this.hi=i,this.ci=r,this.wi=e||cn.DefaultOrgSettings.MaxPerfMarksPerPage},t.prototype.start=function(){var t=this;this.gi=0;var n=window.performance;n&&(this._t.recording.inFrame||this.At.enqueue({Kind:Ot.REC_FEAT_SUPPORTED,Args:[zt.Performance,this.vi,zt.PerformanceEntries,this.li,zt.PerformanceMemory,this.di,zt.PerformanceObserver,!!window.PerformanceObserver,zt.PerformanceTimeOrigin,this.pi,zt.LayoutShift,this.mi,zt.FirstInput,this.yi,zt.LargestContentfulPaint,this.bi,zt.LongTask,this.Ei]}),this.Xn(),!this.qn&&n.addEventListener&&n.removeEventListener&&this.T&&this.T.add(n,"resourcetimingbufferfull",!0,function(){t.At.enqueue({Kind:Ot.RESOURCE_TIMING_BUFFER_FULL,Args:[]})}),this.Ci(),this.Pi())},t.prototype.ji=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(t){switch(t.label){case 0:if(!this.fi||!this.li||0==this.ki.length)return[2];if(this.Ii)return[2];this.Ii=!0,t.label=1;case 1:return t.trys.push([1,,3,4]),[4,this.Oi()];case 2:return t.sent(),[3,4];case 3:return this.Ii=!1,this.ki=[],[7];case 4:return[2];}})})},t.prototype.Mi=function(){return this.Ai?Yn.race([this._i,si(250,1e3)]):this._i},t.prototype.Oi=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){var t,n,i,r,s,o,u,a;return(0,e.__generator)(this,function(e){switch(e.label){case 0:t=0,n=0,i=this.ki,e.label=1;case 1:if(!(nt?[4,this.Mi()]:[3,4]):[3,6];case 3:a=e.sent(),t=p()+Math.max(a.timeRemaining(),15),e.label=4;case 4:this.Ki(u),e.label=5;case 5:return s++,[3,2];case 6:return n++,[3,1];case 7:return[2];}})})},t.prototype.onLoad=function(){this.xi||(this.xi=!0,this.vi&&(this.Ri(performance.timing),this.ji()))},t.prototype.tick=function(){this.Ci()},t.prototype.stop=function(){var t;this.T&&this.T.clear(),this.Ti=void 0;var n=[];this.qn?(this.qn.takeRecords&&(n=this.qn.takeRecords()),this.qn.disconnect()):window.performance&&window.performance.getEntries&&(n=window.performance.getEntries()),n.length>300&&(n=n.slice(0,300),this.At.enqueue({Kind:Ot.RESOURCE_TIMING_BUFFER_FULL,Args:[]})),this.Ci(),null===(t=this.Ai)||void 0===t||t.call(this),this.ki.push(n),this.ji()},t.prototype.Xn=function(){var t=this;if(!this.qn&&this.li&&window.PerformanceObserver){this.ki.push(performance.getEntries()),this.ji(),this.qn=new window.PerformanceObserver(function(n){var i=n.getEntries();t.ki.push(i),t.ji()});var n=[us,as,hs,fs];window.PerformancePaintTiming&&n.push(cs),this.mi&&n.push(vs),this.yi&&n.push(ls),this.bi&&n.push(ds),this.Ei&&n.push(ps),this.qn.observe({entryTypes:n})}},t.prototype.Ci=function(){if(this.di&&!this._t.recording.inFrame){var t=performance.memory;if(t){var n=t.usedJSHeapSize-this.Si;(0==this.Si||o.mathAbs(n/this.Si)>.2)&&(this.Hi(Qt.Memory,t,Vt.Memory),this.Si=t.usedJSHeapSize)}}},t.prototype.Pi=function(){var t={timeOrigin:d.timeOrigin};this.Hi(Qt.TimeOrigin,t,Vt.TimeOrigin)},t.prototype.Ki=function(t){switch(t.entryType){case us:this.Ni(t);break;case as:this.Li(t);break;case cs:this.Ui(t);break;case hs:this.Fi(t);break;case fs:this.Di(t);break;case vs:this.Bi(t);break;case ls:this.Wi(t);break;case ds:this.qi(t);break;case ps:this.Qi(t);}},t.prototype.Ri=function(t){this.Hi(Qt.Timing,t,Vt.Timing)},t.prototype.Ni=function(t){this.Hi(Qt.Navigation,t,Vt.Navigation,{name:us})},t.prototype.Li=function(t){if(this.hi){var n=t.initiatorType;(this.ci||"img"!==n&&"image"!==n)&&this.Hi(Qt.Resource,t,Vt.Resource,{name:n})}},t.prototype.Ui=function(t){this.Hi(Qt.Paint,t,Vt.Measure)},t.prototype.Di=function(t){this.Hi(Qt.Mark,t,Vt.Measure)},t.prototype.Fi=function(t){this.Hi(Qt.Measure,t,Vt.Measure)},t.prototype.Bi=function(t){this.Hi(Qt.LayoutShift,t,Vt.LayoutShift)},t.prototype.Wi=function(t){this.Hi(Qt.FirstInput,t,Vt.FirstInput)},t.prototype.qi=function(t){this.Hi(Qt.LargestContentfulPaint,t,Vt.LargestContentfulPaint)},t.prototype.Qi=function(t){this.Hi(Qt.LongTask,t,Vt.Measure)},t.prototype.Hi=function(t,n,i,r){if(void 0===r&&(r={}),!this.atLimit(t)){for(var e=[t],s=0,o=i;s=this.wi)return!0;this.gi++;}return!1},t}();function gs(t,n){var i,r;return(null!==(r=null===(i=t.PerformanceObserver)||void 0===i?void 0:i.supportedEntryTypes)&&void 0!==r?r:[]).indexOf(n)>-1}function ms(t){var n=0,i={id:n++,edges:{}};return t.split("\n").forEach(function(t){var r=t.trim();if(""!=r){if(0==r.indexOf("/")||r.lastIndexOf("/")==r.length-1)throw new Error("Leading and trailing slashes are not supported");var e=i,s=r.split("/");s.forEach(function(t,i){var r=t.trim();if(""===r)throw new Error("Empty elements are not allowed");if("**"!=r&&"*"!=r&&-1!=r.indexOf("*"))throw new Error("Embedded wildcards are not supported");var o=null;r in e.edges&&(o=e.edges[r]),o||(o={id:n++,edges:{}},e.edges[r]=o),i==s.length-1&&(o.term=!0),e=o})}}),i}var ys=ms("**");function bs(t,n,i){if(!js(i)){try{for(var r=[],e=0,s=i;e=n&&(v?e=void 0:(e="_fs_trimmed_values",v=!0)),f[f.length-1]--,e&&e!==cn.BlockedFieldValue&&s?f.push(o.objectKeys(e).length):c.pop();f[f.length-1]<=0;)f.pop(),c.pop();for(var u=0,a=r;u0&&l!==f.length-1)throw new Error("Property matcher depth out of sync")}return e})}catch(t){Tt.sendToBugsnag(t,"error")}return"[error serializing "+t.constructor.name+"]"}}var Es=function(){function t(t){this.zi=1;var n=[t];t.edges["**"]&&n.push(t.edges["**"]),this.$i=[n]}return t.prototype.Gi=function(){if(this.$i.length<=0)return[];var t=this.$i.length-1,n=this.$i[t];return"number"==typeof n?this.$i[t-1]:n},t.prototype.depth=function(){return this.zi},t.prototype.isRedacted=function(t){var n=this.Gi();return 0===n.length||t&&!n.some(function(t){return t.term})},t.prototype.push=function(t){var n;this.zi++;var i=this.Gi(),r=[];function e(n){n.edges["**"]&&(r.push(n.edges["**"],Ss(n)),e(n.edges["**"])),n.edges["*"]&&r.push(n.edges["*"]),n.edges[t]&&r.push(n.edges[t])}for(var s=0,o=i;s0&&this.zi--;var t=this.$i[this.$i.length-1];"number"==typeof t&&t>1?this.$i[this.$i.length-1]--:this.$i.pop()},t}();function Ss(t){var n=t.edges["**"];if(!n)throw new Error("Node must have double-wildcard edge.");return ot(t.edges,1)?{id:-n.id,edges:{"**":n}}:t}var xs,ks,_s,As=function(){function t(t){this.Xi=t,this.Ji=null}return t.prototype.disable=function(){this.Ji&&(this.Ji.disable(),this.Ji=null)},t.prototype.enable=function(t){var n,i=this,r=T(t),s=null===(n=null==r?void 0:r._w)||void 0===n?void 0:n.fetch;(s||t.fetch)&&(this.Ji=mt(s?r._w:t,"fetch"),this.Ji&&this.Ji.afterSync(function(t){var n=t.result;t.result=(0,e.__awaiter)(i,void 0,void 0,function(){return(0,e.__generator)(this,function(i){switch(i.label){case 0:return i.trys.push([0,2,,3]),[4,this.Zi(n,t.args[0],t.args[1])];case 1:case 2:return i.sent(),[3,3];case 3:return[2,n];}})})}))},t.prototype.Zi=function(t,n,i){return(0,e.__awaiter)(this,void 0,Yn,function(){var r,s,o,u,a,c;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return r="GET",s="",a=!1,"string"==typeof n?s=n:"url"in n?(s=n.url,r=n.method,o=n.body,u=n.headers,a=!!n.signal):s=""+n,s?(i&&(r=i.method||r,u=Ds(i.headers),o=i.body||o,a=!!i.signal||a),c=this.Yi(t),a&&s.search(/\/(graphql|gql)/i)>-1?[4,Yn.race([c,ni(5e3)])]:[3,2]):[2];case 1:e.sent(),e.label=2;case 2:return this.Xi.startRequest(r,s,{body:function(){return o},headers:u},c),[2];}})})},t.prototype.Yi=function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){var n,i,r,s;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return[4,t];case 1:if(n=e.sent(),i=n.headers,r=(i.get("content-type")||"default").split(";")[0],!(["default","text/plain","text/json","application/json"].indexOf(r)>-1))return[2,{status:n.status,data:{headers:i,body:null}}];s=null,e.label=2;case 2:return e.trys.push([2,4,,5]),[4,n.clone().text()];case 3:return s=e.sent(),[3,5];case 4:return e.sent(),[3,5];case 5:return[2,{status:n.status,data:{headers:i,body:s}}];}})})},t}(),Is=function(){function t(t){this.Xi=t,this.tr=new WeakMap}return t.prototype.disable=function(){this.nr&&(this.nr.disable(),this.nr=null),this.ir&&(this.ir.disable(),this.ir=null),this.rr&&(this.rr.disable(),this.rr=null)},t.prototype.er=function(t){var n=this.tr.get(t);if(n)return n;var i={};return this.tr.set(t,i),i},t.prototype.enable=function(t){var n,i,r,s,o=this,u=T(t),a=(null===(n=null==u?void 0:u._w)||void 0===n?void 0:n.XMLHttpRequest)||t.XMLHttpRequest;if(a){var c=a.prototype;this.nr=null===(i=mt(c,"open"))||void 0===i?void 0:i.before(function(t){var n=o.er(t.that);n.method=t.args[0],n.url=t.args[1]}),this.rr=null===(r=mt(c,"setRequestHeader"))||void 0===r?void 0:r.before(function(t){var n=t.that,i=t.args[0],r=t.args[1],e=o.er(n);e.headers||(e.headers=[]),e.headers.push([i,r])}),this.ir=null===(s=mt(c,"send"))||void 0===s?void 0:s.before(function(t){var n=t.that,i=t.args[0],r=o.er(n),s=r.url,u=r.method,a=r.headers;void 0!==s&&void 0!==u&&(o.tr["delete"](n),o.Xi.startRequest(u,s,{headers:Ds(a),body:i},function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){var n;return(0,e.__generator)(this,function(i){switch(i.label){case 0:return[4,new Yn(function(n){t.addEventListener("readystatechange",function(){t.readyState===XMLHttpRequest.DONE&&n()}),t.addEventListener("load",n),t.addEventListener("error",n)})];case 1:return i.sent(),n=function(t){if(t)return{forEach:function(n){for(var i,r=/([^:]*):\s+(.*)(?:\r\n|$)/g;i=r.exec(t);)n(i[2],i[1])}}}(t.getAllResponseHeaders()),[2,{status:t.status,data:{headers:n,body:function(){return"text"===t.responseType?t.responseText:t.response}}}];}})})}(n)))})}},t}(),Ts=/^data:/i,Cs=function(){function t(t,n){this._t=t,this.At=n,this.sr=!1,this.ur=new Ps(t,n),this.ar=new Is(this.ur),this.cr=new As(this.ur)}return t.prototype.isEnabled=function(){return this.sr},t.prototype.start=function(t){t.AjaxWatcher&&(this.sr||(this.sr=!0,this.At.enqueue({Kind:Ot.REC_FEAT_SUPPORTED,Args:[zt.Ajax,!0]}),this.ar.enable(this._t.window),this.cr.enable(this._t.window)))},t.prototype.stop=function(){this.sr&&(this.sr=!1,this.ar.disable(),this.cr.disable())},t.prototype.tick=function(){this.ur.tick()},t.prototype.setWatches=function(t){this.ur.setWatches(t)},t.prototype.initialize=function(t){this.ur.initialize(t)},t}(),Ps=function(){function t(t,n){this._t=t,this.At=n,this.hr=[],this.vr={},this.lr={},this.dr=[],this.pr=0;var i=cn.DefaultOrgSettings;this.initialize({requests:i.HttpRequestHeadersAllowlist,responses:i.HttpResponseHeadersAllowlist,maxAjaxPayloadLength:i.MaxAjaxPayloadLength})}return t.prototype.wr=function(t){for(var n=!1,i=!1,r=[],e=[],s=0,o=this.hr;s-1}function Os(t,n,i){return[t.length,Ns(t,n,i)]}function Ms(t,n,i){var r=void 0;return js(n)||(r=bs(t,i,n)),[Hs(t),r]}function Ks(t,n){var i=t.byteLength,r=void 0;return js(n)||(r="[ArrayBuffer]"),[i,r]}function Rs(t,n,i){return(0,e.__awaiter)(this,void 0,Yn,function(){var r,s,o,u,a;return(0,e.__generator)(this,function(e){switch(e.label){case 0:if(s=(r=t).size,js(n))return[2,[s,void 0]];switch(r.type){case"application/json":case"application/vnd.api+json":case"text/plain":return[3,1];}return[3,4];case 1:return e.trys.push([1,3,,4]),[4,r.text()["catch"](function(t){Tt.sendToBugsnag(t,"warning")})];case 2:return(o=e.sent())&&(u=Ns(o,n,i))?[2,[s,u]]:[3,4];case 3:return a=e.sent(),Tt.sendToBugsnag(a,"warning"),[3,4];case 4:return[2,[s,"[Blob]"]];}})})}function Hs(t){try{return o.jsonStringify(t).length}catch(t){}return 0}function Ns(t,n,i){if(!js(n))try{return bs(o.jsonParse(t),i,n)}catch(r){return n.length>0&&n.every(function(t){return!0===t})?t.slice(0,i):void 0}}function Ls(t,n){switch(t){default:case Zt.Elide:return!1;case Zt.Record:return!0;case Zt.Allowlist:try{return ms(n)}catch(t){return!1}}}function Us(t,n,i,r){var s;return(0,e.__awaiter)(this,void 0,Yn,function(){var o,u,a,c,h,f,v;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return o="",null===(s=r.headers)||void 0===s||s.forEach(function(n,i){var r=i.toLowerCase(),e=t[r];o+=r+(e?": "+n:"")+"\r\n"}),"function"!=typeof(u=null==r?void 0:r.body)?[3,2]:[4,u()];case 1:return a=e.sent(),[3,3];case 2:a=u,e.label=3;case 3:return[4,Fs(n,a,i)];case 4:return c=e.sent(),h=c[0],f=c[1],v=0!==h||f?qt.NotEmpty:qt.Unknown,[2,{headers:o,text:f,size:h,legibility:v}];}})})}function Fs(t,n,i){return void 0===i&&(i=cn.DefaultOrgSettings.MaxAjaxPayloadLength),(0,e.__awaiter)(this,void 0,Yn,function(){var r;return(0,e.__generator)(this,function(e){if(null==n)return[2,[0,void 0]];switch(typeof n){default:return[2,[-1,js(t)?void 0:"[unknown]"]];case"string":return[2,Os(n,t,i)];case"object":switch(r=n.constructor){case Object:default:return[2,Ms(n,t,i)];case Blob:return[2,Rs(n,t,i)];case ArrayBuffer:return[2,Ks(n,t)];case Document:case FormData:case URLSearchParams:case ReadableStream:return[2,[-1,js(t)?void 0:""+r.name]];}}return[2]})})}function Ds(t){return t?tt(t)?{forEach:function(n){for(var i=0,r=t;i-1){if(n.unshift(e),r instanceof CSSStyleSheet)break;i=r}else Tt.sendToBugsnag("Could not find intermediate rule in parent","warning")}return n},t.prototype.Wr=function(t,n){for(var i=0;i=t?(this.oe-=t,[!0,0]):[!1,(t-this.oe)/this.ne]},t}())(2,2e5),ho=new Set(["measureText","getImageData","getError","getTransform","isContextLost","isEnabled","isFramebuffer","isProgram","isRenderbuffer","isShader","isTexture"]),fo=new Set(["fillText"]),vo=function(){function t(t,n,i,r){this.At=n,this.Ti=i,this.ai=r,this.ue=dn.CaptureCanvasOps,this.ae=[],this.ce=[],this.he=new WeakMap,this.fe=new WeakMap,this.ve=new Set,this.le=0,this.de=new WeakMap,this.pe=!1,this.we=new WeakMap,this.ge=new Set,this.me=new WeakMap,this.ye=1,this.be=new WeakMap,this.Ee=1,this.Se=0,this.xe=!1}return t.prototype.start=function(t){var n,i=this;if(t.CanvasWatcherMode&&(this.At.enqueue({Kind:Ot.REC_FEAT_SUPPORTED,Args:[zt.CanvasWatcherEnabled,!0]}),this.pe=!0,this.ue=null!==(n=t.CanvasWatcherMode)&&void 0!==n?n:dn.CaptureCanvasOps,this.Ji("2d",CanvasRenderingContext2D),this.Ji("webgl",WebGLRenderingContext),this.ue===dn.ScreenshotCanvas)){if(!HTMLCanvasElement.prototype.toDataURL)return;this.le=setInterval(function(){return i.screenshotConnectedCanvases()},1e3)}},t.prototype.ke=function(t,n){return"object"!=typeof n?[void 0,0]:(this.be.has(n)||this.be.set(n,[t,this.Ee++]),this.be.get(n))},t.prototype.Ji=function(t,n){var i=this;if(n)for(var r=n.prototype,e=function(e){if(ho.has(e))return"continue";var o=Object.getOwnPropertyDescriptor(r,e);if("function"==typeof(null==o?void 0:o.value)){var u=mt(r,e);u&&(u.afterSync(function(n){return i._e(t,e,n.that,n.args,n.result)}),s.ae.push(u))}else"function"==typeof(null==o?void 0:o.set)&&s.ce.push(yt(n,e,s.Ae(t,e)))},s=this,o=0,u=Object.keys(r);o0){var o=n;if(!o){var u=t instanceof HTMLCanvasElement?Ui(t):void 0,a=t instanceof HTMLCanvasElement&&St(t);o=null!==(r=null==u?void 0:u.mask)&&void 0!==r?r:a}this.Pe(t,e,s,o)}return e}},t.prototype.je=function(t,n,i,r,e,s,o){var u;switch(typeof r){case"string":return e?Ci(r):r;case"number":case"boolean":case"bigint":return r;case"undefined":return{undef:!0};case"object":if(!r)return r;try{o.set(r,!0)}catch(t){}var a=null===(u=Object.getPrototypeOf(r))||void 0===u?void 0:u.constructor,c=(null==a?void 0:a.name)||function(t){var n;if(t){var i=t.toString(),r=po.exec(i);return r||(r=wo.exec(i)),null===(n=null==r?void 0:r[1])||void 0===n?void 0:n.trim()}}(a),h={ctor:c};if(r instanceof Node&&(l=Di(r)))return h.id=l,h;switch(c){case"Array":return this.Se+=r.length,this.Oe(t,n,i,r,e,s,o);case"CanvasGradient":return h;case"HTMLImageElement":var f=he(r.src,{source:"dom",type:"canvas"});return this.ai.record(f),h.src=f,h;case"HTMLCanvasElement":var v=r,l=this.flush(v,e);return h.srcId=l,h;}if(function(t){var n;return!!(null===(n=Object.prototype.toString.call(t))||void 0===n?void 0:n.match(lo))}(r))return this.be.has(r)?this.Me(r,h,e):(h.typedArray="["+r.toString()+"]",this.Se+=r.length,h);if("object"==typeof r&&this.be.has(r))return this.Me(r,h,e);if(r instanceof WebGLBuffer||r instanceof WebGLTexture){var d=void 0;switch(s){case"bindTexture":d=this.Ke(t,"createTexture",n,i,r);break;case"bindBuffer":d=this.Ke(t,"createBuffer",n,i,r);}if(void 0!==d)return this.Me(r,h,e)}var p=r;for(var w in h.obj={},p){try{switch(typeof p[w]){case"function":continue;case"object":if(p[w]&&o.has(p[w]))continue;}}catch(t){continue}++this.Se,h.obj[w]=this.je(t,n,i,p[w],e,s,o)}return h;default:return null;}},t.prototype.Me=function(t,n,i){var r=this.be.get(t),e=r[0],s=r[1];return this.flush(e,i),n.ref=s,delete n.ctor,n},t.prototype.Ke=function(t,n,i,r,e){var s=this.ke(i,e),o=(s[0],s[1]);return this.Re(r,[[t,ln.Function,n,[],o]]),o},t.prototype.Oe=function(t,n,i,r,e,s,o){var u=this;return void 0===o&&(o=new WeakMap),this.Se+=r.length+1,r.map(function(r){return u.je(t,n,i,r,e,s,o)})},t.prototype.Pe=function(t,n,i,r){var e=this;if(void 0===r&&(r=!1),!this.xe){var s=i.map(function(i){var s=i[0],o=i[1],u=i[2],a=i[3],c=i[4];return[s,o,u,e.Oe(s,t,n,a,r&&fo.has(u),u),c]});if(!this.he.has(t)&&(this.he.set(t,!0),i.some(function(t){return"2d"===t[0]}))){var o=this.He(t);if(o.length>0)return o.push.apply(o,s),void this.Re(n,o)}this.Re(n,s)}},t.prototype.Re=function(t,n){if(!this.xe){var i=co.hasCapacityFor(this.Se),r=i[0];i[1],this.Se=0,r?this.At.enqueue({Kind:Ot.CANVAS,Args:[t,n]}):this.xe=!0}},t.prototype.He=function(t){var n=t.getContext("2d");if(!n)return[];var i=[];if((n instanceof CanvasRenderingContext2D||n instanceof OffscreenCanvasRenderingContext2D)&&"function"==typeof n.getTransform){var r=n.getTransform();if(!r.isIdentity){var e=r.a,s=r.b,o=r.c,u=r.d,a=r.e,c=r.f;i.push(["2d",ln.Function,"transform",[e,s,o,u,a,c],-1])}}return i},t.prototype.Ne=function(t,n){t instanceof HTMLCanvasElement&&(this.ue===dn.ScreenshotCanvas?(this.fe.set(t,!0),this.ve.add(t)):(this.ge.add(t),this.Ie(t,n)))},t.prototype._e=function(t,n,i,r,e){for(var s=[],o=0;o))/m,mo=/^(eval@)?(\[native code\])?$/;function yo(t){if(!t||"string"!=typeof t.stack)return[];var n=t;return n.stack.match(go)?n.stack.split("\n").filter(function(t){return!!t.match(go)}).map(function(t){var n=t;n.indexOf("(eval ")>-1&&(n=n.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(\),.*$)/g,""));var i=n.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/\(native code\)/,"").split(/\s+/).slice(1),r=Eo(i.pop());return bo(i.join(" "),["eval",""].indexOf(r[0])>-1?"":r[0],r[1],r[2])}):n.stack.split("\n").filter(function(t){return!t.match(mo)}).map(function(t){var n=t;if(n.indexOf(" > eval")>-1&&(n=n.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),-1===n.indexOf("@")&&-1===n.indexOf(":"))return[n,"",-1,-1];var i=n.split("@"),r=Eo(i.pop());return bo(i.join("@"),r[0],r[1],r[2])})}function bo(t,n,i,r){return[t||"",n||"",parseInt(i||"-1",10),parseInt(r||"-1",10)]}function Eo(t){if(!t||-1===t.indexOf(":"))return["","",""];var n=/(.+?)(?::(\d+))?(?::(\d+))?$/.exec(t.replace(/[()]/g,""));return n?[n[1]||"",n[2]||"",n[3]||""]:["","",""]}var So,xo,ko=["log","info","warn","error","debug","_fs_debug","assert","trace"],_o=ko.filter(function(t){return!/debug/.test(t)}),Ao=function(t,n,i){void 0===i&&(i=!0);var r=Qi(t,n);return i?be(r):r},Io=function(){function t(t,n,i){this.At=n,this.sr=!1,this.Fe=!1,this.De=0,this.Dt=[],this.Be=cn.DefaultOrgSettings.MaxConsoleLogPerPage,this.Dn=t.window,this.T=i.createChild()}return t.prototype.initializeMaxLogsPerPage=function(t){this.Be=t||cn.DefaultOrgSettings.MaxConsoleLogPerPage},t.prototype.We=function(){return"\"[received more than "+this.Be+" messages]\""},t.prototype.start=function(t){var n=this;if(t.ConsoleWatcher&&(this.T.add(this.Dn,"error",!0,function(t){return n.addError(t)}),this.T.add(this.Dn,"unhandledrejection",!0,function(t){n.addError({error:t.reason,message:"Uncaught (in promise)",filename:"",lineno:0,colno:0})},!0),!this.sr))if(this.sr=!0,this.At.enqueue({Kind:Ot.REC_FEAT_SUPPORTED,Args:[zt.Console,!0]}),this.Dn.console)for(var i=function(t){var i=mt(r.Dn.console,t);if(!i)return"continue";"assert"===t?i.before(function(i){var r=i.args;r[0]||n.qe(t,Array.prototype.slice.apply(r,[1]))}):i.before(function(i){var r=i.args;return n.qe(t,r)}),r.Dt.push(i)},r=this,e=0,s=_o;e5e5)return!1;var i=Ws(Bs(t));return!!i&&(!!("style"===ki(t)&&i.length>0&&Xs.test(n))||function(t){var n;try{if((null===(n=t.classList)||void 0===n?void 0:n.contains("fs-css-in-js"))||t.hasAttribute("data-fela-type")||t.hasAttribute("data-aphrodite"))return!0}catch(t){Tt.sendToBugsnag(t,"error")}return!1}(t))}(s)&&(null==n||n.push(function(){u.snapshotEl(s),"link"===ki(s)&&i.T.add(s,"load",!1,function(){u.snapshotEl(s)})}));break;case"CANVAS":this._t.measurer.requestMeasureTask(ii.Low,function(){return i.us[So.Canvas].flush(t)});break;default:t.nodeName&&"#"!==t.nodeName[0]&&t.nodeName.indexOf("-")>-1&&this.us[So.CustomElement].onCustomNodeVisited(t);}if("scrollLeft"in t&&"scrollTop"in t){var a=t;this._t.measurer.requestMeasureTask(ii.Low,function(){0==a.scrollLeft&&0==a.scrollTop||i.Ps(a)})}null==n||n.push(function(){i._t.measurer.requestMeasureTask(ii.Low,function(){i.us[So.Animation].snapshot(t)})})},t.prototype.On=function(t){var n,i=t.node,r=ki(t.node);if("iframe"===r)this.$e(t.node);else if("function"==typeof i.getElementsByTagName)for(var e=null!==(n=i.getElementsByTagName("iframe"))&&void 0!==n?n:[],s=0;s-1&&s.push(i.href),("img"===t||"source"===t)&&(e=i.srcset)&&null==e.match(/^\s*$/))for(var c=0,h=e.split(",");c0)return i[0]}}return t.target}function Mo(t){var n;return!!(null!==(n=t._fs_trust_event)&&void 0!==n&&n||t.isTrusted)}var Ko,Ro=function(){function t(t,n){this.Vr=t,this.Gs=n,this.Xs=[],this.Js=0}return t.prototype.add=function(t){this.Xs.length>0&&this.Xs[this.Xs.length-1].When===t.When&&this.Xs.pop(),0===this.Xs.length?(this.Vr.push(t),this.Js=t.When):t.When>this.Js&&(this.Js=t.When),this.Xs.push(t)},t.prototype.finish=function(t,n){void 0===n&&(n=[]);var i=this.Xs.length;if(i<=1)return!1;for(var r=[],s=this.Xs[0].When,o=this.Xs[i-1].When,u=o-s!=0?o-s:1,a=0;a0&&this.Zs--,Lo(this.Wn.prev)},t.prototype.shift=function(){return this.Zs>0&&this.Zs--,Lo(this.Wn.next)},t}();function No(t,n){var i=t.next;n.next=i,n.prev=t,t.next=i.prev=n}function Lo(t){var n=t.prev,i=t.next;return n.next=i,i.prev=n,t.value}!function(t){t[t.rageWindowMillis=2e3]="rageWindowMillis",t[t.defaultRageThreshold=5]="defaultRageThreshold",t[t.rageThresholdIfPageChanges=8]="rageThresholdIfPageChanges",t[t.thresholdChangeQuiescenceMillis=2e3]="thresholdChangeQuiescenceMillis"}(Ko||(Ko={}));var Uo=function(){function t(t,n){var i,r;void 0===n&&(n=w),this._t=t,this.Ys=n,this.no=new Ho,this.io=Ko.defaultRageThreshold,this.ro=-1,this.eo=new WeakMap;var e=t.recording.pageResponse();if(!e)throw new Error("Attempt to construct EasyBake before rec/page response is set.");for(var s=[".fs-ignore-rage-clicks",".fs-ignore-rage-clicks *"],o=0,u=null!==(r=null===(i=e.BehaviorSignalSettings)||void 0===i?void 0:i.ElementBlocks)&&void 0!==r?r:[];o-1&&(s.push(a.Selector),s.push(a.Selector+" *"))}var c=s.join(", ");Be(c)?this.so=[c]:this.so=s}return t.prototype.oo=function(t){var n=this.eo.get(t);if(void 0!==n)return n;for(var i=0,r=this.so;i=this.io){var a=this._t.recording.getCurrentSessionURL,c={eventStartTimeStamp:this.no.first(),eventEndTimeStamp:i,eventReplayUrlAtStart:a(),eventReplayUrlAtCurrentTime:a(!0)};this.dispatchRageClickEvent(r,c),this.io=Ko.defaultRageThreshold,this.no=new Ho}}}}}},t.prototype.dispatchRageClickEvent=function(t,n){var i,r="fullstory/rageclick";try{i=new CustomEvent(r,{detail:n,bubbles:!0,cancelable:!0})}catch(t){(i=document.createEvent("customevent")).initCustomEvent(r,!0,!0,n)}o.setWindowTimeout(window,Tt.wrap(function(){t.dispatchEvent(i)}),0)},t}(),Fo=function(){function t(t){this._t=t,this.uo=this._t.time.wallTime(),this.ao=!1}return t.prototype.getLastUserAcitivityTS=function(){return this.uo},t.prototype.getMsSinceLastUserAcivity=function(){return o.mathFloor(this._t.time.wallTime()-this.uo)},t.prototype.resetUserActivity=function(){this.uo=this._t.time.wallTime()},t.prototype.isHibernating=function(){return this.ao},t.prototype.setHibernating=function(){this.ao=!0},t}(),Do=function(){function t(t,n,i,r){void 0===r&&(r=Yi),this._t=t,this.co=n,this.At=i,this.ho=!1,this.fo=!1,this.vo=cn.HeartbeatInitial,this.lo=cn.PageInactivityTimeout,this.heartbeatTimeout=new r(this["do"].bind(this)),this.hibernationTimeout=new r(this.po.bind(this),this.lo)}return t.prototype.getUserActivityModel=function(){return this.co},t.prototype.manualHibernateCheck=function(){this.co.isHibernating()||this.co.getMsSinceLastUserAcivity()>=cn.PageInactivityTimeout+5e3&&this.po()},t.prototype.scanEvents=function(t){if(!this.ho){this.manualHibernateCheck();for(var n=!1,i=0,r=t;icn.HeartbeatMax&&(this.vo=cn.HeartbeatMax),this.heartbeatTimeout.start(this.vo)},t.prototype.po=function(){if(!this.co.isHibernating()){var t=!1;this.co.getMsSinceLastUserAcivity()<=2*cn.PageInactivityTimeout?this.At.enqueue({Kind:Ot.UNLOAD,Args:[Wt.Hibernation]}):t=!0;try{this.ho=!0,this.co.setHibernating(),this.shutdown(),this.At.onHibernate(t)}finally{this.ho=!1}}},t.prototype.wo=function(){this.fo||(this.fo=!0,this._t.recording.splitPage(Wt.Hibernation))},t}(),Bo=function(){function t(t,n,i,r,e,s){void 0===r&&(r=function(){return[]}),void 0===e&&(e=Zi),void 0===s&&(s=Yi),this._t=t,this.mo=n,this.yo=r,this.bo=e,this.Eo=0,this.So=[],this.xo=!1,this.ko=!1,this._o=0,this.Ao=-1,this.Io=!1,this.Qt=[],this.To=new this.bo(cn.CurveSamplingInterval),this.Co=new this.bo(cn.MutationProcessingInterval),i&&(this.Po=new Do(this._t,i,this,s))}return t.prototype.startPipeline=function(t){var n;return(0,e.__awaiter)(this,void 0,Yn,function(){var i,r=this;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return this.ko||this.xo?[2]:(this.xo=!0,t.frameId&&(this.Eo=t.frameId),t.parentIds&&(this.So=t.parentIds),i=!0,[4,ei()]);case 1:return e.sent(),this.processEvents(),[4,ei()];case 2:return e.sent(),window,this.Co.start(function(){window,r.processEvents(),window}),this.To.start(function(){window,r.processEvents(i),window}),null===(n=this.Po)||void 0===n||n.start(),this.mo.startPipeline(t),window,[2];}})})},t.prototype.enableEasyBake=function(){this.jo=new Uo(this._t)},t.prototype.enqueueSimultaneousEventsIn=function(t){if(0===this._o){var n=this._t.time.now();this.Ao=n>this.Ao?n:this.Ao}try{return this._o++,t(this.Ao)}finally{this._o--,this._o<0&&(this._o=0)}},t.prototype.enqueue=function(t){var n=this._o>0?this.Ao:this._t.time.now();this.Oo(n,t),Ji.checkForBrokenSchedulers()},t.prototype.Oo=function(t,n){var i;if(!this.ko){var r=t;r0){var n=t;n.When=this.Qt[0].When,this.Qt.unshift(n)}else this.enqueue(t)},t.prototype.addUnload=function(t){this.Io||(this.Io=!0,this.enqueue({Kind:Ot.UNLOAD,Args:[t]}),this.singSwanSong(t))},t.prototype.shutdown=function(t){this.addUnload(t),this.Mo(),this.ko=!0,this.Ko()},t.prototype.Mo=function(){this.processEvents(),this.mo.flush()},t.prototype.singSwanSong=function(t){this.ko||(window,this.Mo(),t===Wt.Hidden&&this.Io||this.mo.singSwanSong(),window)},t.prototype.rebaseIframe=function(t,n){for(var i=Math.max(0,n),r=this._t.time.startTime(),e=function(n){var e=r+n-t;return e>=i?e:i},s=0,o=this.Qt;s0){var f=h[h.length-1].Args[2];f&&(h[0].Args[9]=f)}}for(var v in s)s[l=parseInt(v,10)].finish(Ot.SCROLL_LAYOUT_CURVE,[l]);for(var v in o)o[l=parseInt(v,10)].finish(Ot.SCROLL_VISUAL_OFFSET_CURVE,[l]);for(var v in e){var l;e[l=parseInt(v,10)].finish(Ot.TOUCHMOVE_CURVE,[l])}return n&&n.finish(Ot.RESIZE_VISUAL_CURVE),i}(n);t||(i=i.concat(this.yo())),this.Ro(i),this.sendEvents(this._t.recording.pageSignature(),i)}},t.prototype.sendEvents=function(t,n){var i;0!=n.length&&(null===(i=this.Po)||void 0===i||i.scanEvents(n),this.mo.enqueueEvents(t,n))},t.prototype.onHibernate=function(t){t||this.Mo(),this.mo.singSwanSong(),this.mo.stopPipeline()},t.prototype.Ro=function(t){if(this.Eo)for(var n=this.So,i=n&&n.length>0,r=0;r>>0).toString(16)).slice(-8);return t},t}();function qo(t){var n=new Wo(1);return n.writeAscii(t),n.sumAsHex()}function Qo(t){var n=new Uint8Array(t);return Vo(String.fromCharCode.apply(null,n))}function Vo(t){var n;return(null!==(n=window.btoa)&&void 0!==n?n:zo)(t).replace(/\+/g,"-").replace(/\//g,"_")}function zo(t){for(var n=String(t),i=[],r=0,e=0,s=0,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";n.charAt(0|s)||(o="=",s%1);i.push(o.charAt(63&r>>8-s%1*8))){if((e=n.charCodeAt(s+=3/4))>255)throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");r=r<<8|e}return i.join("")}function $o(t,n,i,r){return void 0===r&&(r=new Wo),(0,e.__awaiter)(this,void 0,Yn,function(){var s,o,u,a;return(0,e.__generator)(this,function(e){switch(e.label){case 0:s=t.now(),o=i.byteLength,u=0,e.label=1;case 1:return u25?[4,n(100)]:[3,3]:[3,5];case 2:e.sent(),s=t.now(),e.label=3;case 3:a=new Uint8Array(i,u,Math.min(o-u,1e4)),r.write(a),e.label=4;case 4:return u+=1e4,[3,1];case 5:return[2,{hash:r.sum(),hasher:r}];}})})}var Go=6e6,Xo="resource-uploader",Jo=function(){function t(t,n,i,r,e){void 0===r&&(r=window.FormData),void 0===e&&(e=Yi),this._t=t,this.At=n,this.Ho=i,this.No=r,this.Lo=e,this.pe={},this.Uo={},this.Fo=!1,this.Do=[]}return t.prototype.init=function(){this.No&&this.Bo()["catch"](function(t){Tt.sendToBugsnag(t,"error")})},t.prototype.Bo=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){var t,n,i,r,s,o,u,a,c,h,f,v,l,d,p,w,g,m,y,b,E,S,x,k,_;return(0,e.__generator)(this,function(e){switch(e.label){case 0:t=this._t.options.orgId,e.label=1;case 1:return[4,this.Wo()];case 2:for(n=e.sent(),i={fsnv:{},sha1:{}},r={},s=0,o=n;sGo){var r=he(t,{source:"log",type:"bugsnag"});return Tt.sendToBugsnag("Size of blob resource exceeds limit","warning",{url:r,MaxResourceSizeBytes:Go}),void i(null)}(function(t){var n=ti(),i=n.resolve,r=n.promise,e=new FileReader;return e.readAsArrayBuffer(t),e.onload=function(){i(e.result)},e.onerror=function(t){Tt.sendToBugsnag(t,"error"),i(null)},r})(n).then(function(t){i(t?{buffer:t,blob:n,contentType:n.type}:null)})},e.send(),r)}function Yo(t,n){var i,r;return(0,e.__awaiter)(this,void 0,Yn,function(){var s;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return s=t.window,(null===(r=null===(i=s.crypto)||void 0===i?void 0:i.subtle)||void 0===r?void 0:r.digest)?[4,s.crypto.subtle.digest({name:"sha-1"},n)]:[3,2];case 1:return[2,{hash:Qo(e.sent()),algorithm:"sha1"}];case 2:return[4,$o(t.time,ni,n)];case 3:return[2,{hash:e.sent().hash,algorithm:"fsnv"}];}})})}var tu=/^data:([^;,]*)(;?charset=[^;]+)?(?:;base64)?$/i,nu="Could not parse data url",iu=function(t,n,i){this.name="ProtocolError",this.message=n,this.status=t,this.data=i};function ru(t){return t>=400&&502!==t||202==t||206==t}var eu=function(){function t(t){this.Vo=0,this.zo=t.options.scheme,this.$o=t.options.cdnHost,this._t=t}return t.prototype.page=function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(n){switch(n.label){case 0:return[4,uu(this.zo,vu(this._t),"/rec/page",vt(t))];case 1:return[2,pt(n.sent().text)];}})})},t.prototype.settings=function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){var n;return(0,e.__generator)(this,function(i){return n=t.previewMode||t.fallback?vu(this._t):this.$o,[2,fu(this.zo,n,t)]})})},t.prototype.bundle=function(t){var n;return(0,e.__awaiter)(this,void 0,Yn,function(){var i,r,s,o;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return[4,ei()];case 1:return e.sent(),window,i=vt(t.bundle),this.Vo+=i.length,this.Vo,window,i.length>2e6?[4,ei()]:[3,3];case 2:e.sent(),e.label=3;case 3:return window,r=ou(t.bundle.Seq,t),[4,uu(this.zo,null!==(n=t.recHost)&&void 0!==n?n:vu(this._t),r,i)];case 4:return s=e.sent().text,o=pt(s),window,[2,[this.Vo,o]];}})})},t.prototype.bundleBeacon=function(t){var n;return hu(this.zo,null!==(n=t.recHost)&&void 0!==n?n:vu(this._t),t)},t.prototype.exponentialBackoffMs=function(t,n){var i=o.mathMin(cn.BackoffMax,5e3*o.mathPow(2,t));return n?i+.25*o.mathRandom()*i:i},t}(),su=function(){function t(t){this.zo=t.options.scheme,this._t=t}return t.prototype.uploadResource=function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(n){switch(n.label){case 0:return[4,uu(this.zo,vu(this._t),"/rec/uploadResource",t)];case 1:return[2,n.sent().text];}})})},t.prototype.queryResources=function(t){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(n){switch(n.label){case 0:return[4,uu(this.zo,vu(this._t),"/rec/queryResources",vt(t))];case 1:return[2,pt(n.sent().text)];}})})},t}();function ou(t,n){var i="/rec/bundle"+("v2"===n.version?"/v2":"")+"?OrgId="+n.orgId+"&UserId="+n.userId+"&SessionId="+n.sessionId+"&PageId="+n.pageId+"&Seq="+t;return null!=n.serverPageStart&&(i+="&PageStart="+n.serverPageStart),null!=n.serverBundleTime&&(i+="&PrevBundleTime="+n.serverBundleTime),null!=n.lastUserActivity&&(i+="&LastActivity="+n.lastUserActivity),n.isNewSession&&(i+="&IsNewSession=true"),null!=n.deltaT&&(i+="&DeltaT="+n.deltaT),i}function uu(t,n,i,r){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(e){return[2,cu("POST",t,n,lu(i),!0,r)]})})}function au(t,n,i){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(r){return[2,cu("GET",t,n,lu(i),!1)]})})}function cu(t,n,i,r,s,o){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(e){return[2,new Yn(function(e,u){var a="//"+i+r,c=!1,h=new XMLHttpRequest,f=("withCredentials"in h);jt(f,"XHR missing CORS support"),f&&(h.onreadystatechange=function(){if(4==h.readyState){if(c)return;c=!0;try{var t={text:h.responseText};if(200==h.status)return void e(t);var n=void 0;try{n=pt(t.text)}catch(t){}u(new iu(h.status,t.text,n))}catch(t){Tt.sendToBugsnag(t,"error"),u(t)}}},h.open(t,n+a,!0),h.withCredentials=s,o&&"function"!=typeof o.append&&h.setRequestHeader("Content-Type","text/plain"),h.send(o))})]})})}function hu(t,n,i){if("function"==typeof navigator.sendBeacon){var r=t+"//"+n+ou(i.bundle.Seq,i)+"&SkipResponseBody=true",e=vt(i.bundle);try{return navigator.sendBeacon.bind(navigator)(r,e)}catch(t){}}return!1}function fu(t,n,i){var r;return(0,e.__awaiter)(this,void 0,Yn,function(){var s,o;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return s=null!==(r=i.version)&&void 0!==r?r:"v1",o=i.previewMode?"?previewMode=true":"",[4,au(t,n,"/s/settings/"+i.orgId+"/"+s+"/web"+o)];case 1:return[2,pt(e.sent().text)];}})})}function vu(t){var n,i=null===(n=t.recording.pageResponse())||void 0===n?void 0:n.GCLBSubdomain,r=t.options.recHost;return i&&K(r)?r.replace(/^rs\./,i+"."):r}function lu(t){if(!window.Zone)return t;var n="?";return t.indexOf(n)>-1&&(n="&"),""+t+n+"ngsw-bypass=true"}var du,pu=function(){function t(t,n,i){void 0===i&&(i=new wu),this._t=t,this.Vr=n,this.Go=i}return t.prototype.initialize=function(t){var n;if(t){this.Xo(t);var i=null===(n=this._t.window.location)||void 0===n?void 0:n.href;this.onNavigate(i)}},t.prototype.onNavigate=function(t){return!!this.Go.matches(t)&&(this.Vr.enqueue({Kind:Ot.KEEP_URL,Args:[this.Jo(t)]}),!0)},t.prototype.onClick=function(t){var n;return!!(null===(n=null==t?void 0:t.watchKind)||void 0===n?void 0:n.has(_i.Keep))&&(this.Vr.enqueue({Kind:Ot.KEEP_ELEMENT,Args:[t.id]}),!0)},t.prototype.urlMatches=function(t){return this.Go.matches(t)},t.prototype.Xo=function(t){this.Go.setRules(t)},t.prototype.Jo=function(t){return he(t,{source:"page",type:"base"})},t}(),wu=function(){function t(){this.Zo=null}return t.prototype.setRules=function(t){var n=t.map(function(t){return t.Regex}).filter(this.Yo);n.length>0&&(this.Zo=this.tu(n))},t.prototype.matches=function(t){return!!this.Zo&&this.Zo.test(t)},t.prototype.Yo=function(t){try{return new RegExp(t),!0}catch(n){return Tt.sendToBugsnag("Browser rejected UrlKeep.Regex","error",{expr:t,error:n.toString()}),!1}},t.prototype.tu=function(t){try{return new RegExp("("+t.join(")|(")+")","i")}catch(n){return Tt.sendToBugsnag("Browser rejected joining UrlKeep.Regexs","error",{exprs:t,error:n.toString()}),null}},t}(),gu=function(t){var n=(void 0===t?{}:t).wnd,i=void 0===n?window:n;!function(t,n,i,r,e,s,o,u){var a,c;function h(t){var n,i=[];function r(){n&&(i.forEach(function(t){var i;try{i=t[n[0]]&&t[n[0]](n[1])}catch(n){return void(t[3]&&t[3](n))}i&&i.then?i.then(t[2],t[3]):t[2]&&t[2](i)}),i.length=0)}function e(t){return function(i){n||(n=[t,i],r())}}return t(e(0),e(1)),{then:function(t,n){return h(function(e,s){i.push([t,n,e,s]),r()})}}}(!(i in t)||(t.console&&t.console.log&&t.console.log("FullStory namespace conflict. Please set window[\"_fs_namespace\"]."),0))&&(u=t[i]=function(){var t=function(t,i,r){function e(e,s){n(t,i,r,e,s)}var s=/Async$/;return s.test(t)?(t=t.replace(s,""),"function"==typeof Promise?new Promise(e):h(e)):n(t,i,r)};function n(n,i,r,e,s){return t._api?t._api(n,i,r,e,s):(t.q&&t.q.push([n,i,r,e,s]),null)}return t.q=[],t}(),function(){function t(){}function n(t,n,i){u("setProperties",{type:t,properties:n},i)}function i(t,i){n("user",t,i)}function r(t,n,r){i({uid:t},r),n&&i(n,r)}u.identify=r,u.setUserVars=i,u.identifyAccount=t,u.clearUserCookie=t,u.setVars=n,u.event=function(t,n,i){u("trackEvent",{name:t,properties:n},i)},u.anonymize=function(){r(!1)},u.shutdown=function(){u("shutdown")},u.restart=function(){u("restart")},u.log=function(t,n){u("log",{level:t,msg:n})},u.consent=function(t){u("setIdentity",{consent:!arguments.length||t})}}(),a="fetch",c="XMLHttpRequest",u._w={},u._w[c]=t[c],u._w[a]=t[a],t[a]&&(t[a]=function(){return u._w[a].apply(this,arguments)}),u._v="2.0.0")}(i,i.document,i._fs_namespace,0,0,i._fs_script)};function mu(t,n){if(t&&t.postMessage)try{t.postMessage(function(t){var n;return vt(((n={}).__fs=t,n))}(n),"*")}catch(t){Ut("postMessage",t)}}function yu(t){try{var n=pt(t);if("__fs"in n)return n.__fs}catch(t){}return[du.Unknown]}function bu(t,n,i,r){var e=W(t);if(!e)return!1;try{e.send(n,i,r)}catch(t){e.send(n,i)}return!0}!function(t){t.EndPreviewMode="EndPreviewMode",t.EvtBundle="EvtBundle",t.GreetFrame="GreetFrame",t.InitFrameMobile="InitFrameMobile",t.RequestFrameId="RequestFrameId",t.RestartFrame="RestartFrame",t.SetConsent="SetConsent",t.SetFrameId="SetFrameId",t.ShutdownFrame="ShutdownFrame",t.Unknown="Unknown"}(du||(du={}));var Eu=new RegExp(/^\s+$/),Su=/^fb\d{18}$/,xu=function(t){var n=t.frame,i=t.orgId,r=t.scheme,e=t.script,s=t.recHost,u=t.cdnHost,a=t.appHost,c=t.namespace,h=(t.desc,t.snippetVersion);try{if(function(t){return t.id==t.name&&Su.test(t.id)}(n))return Rt.BlocklistedFrame;if(function(t){return!(t.contentDocument&&t.contentWindow&&t.contentWindow.location)||function(t){return!!t.src&&"about:blank"!=t.src&&t.src.indexOf("javascript:")<0}(t)&&t.src!=t.contentWindow.location.href&&"loading"==t.contentDocument.readyState}(n))return Rt.PartiallyLoaded;var f=n.contentWindow,v=n.contentDocument;if(!f||!v)return Rt.MissingWindowOrDocument;if(!v.head)return Rt.MissingDocumentHead;if(!v.body||0===v.body.childNodes.length)return Rt.MissingBodyOrChildren;for(var l=!1,d=v.body.childNodes,p=0;p0&&(null!==(s=null===(e=null===(r=t.OrgSettings)||void 0===r?void 0:r.UrlPrivacyConfig)||void 0===e?void 0:e.length)&&void 0!==s?s:0)>0&&(null!==(a=null===(u=null===(o=t.OrgSettings)||void 0===o?void 0:o.AttributeBlocklist)||void 0===u?void 0:u.length)&&void 0!==a?a:0)>0;return c||Tt.sendToBugsnag("Invalid page response","error",{rsp:t}),c},t.prototype.handleResponse=function(t,n){var i,r,e,s,o=t.Flags,u=o.AjaxWatcher,a=o.ClientSideRageClick,c=o.GetCurrentSession,h=o.ResourceUploading,f=o.UseClientSideId;this.ku=t,this.Pu=t.UserIntId,this.ju=t.SessionIntId,this.Ou=t.PageIntId,this.Mu=t.PageStart,this.pu=c?_u.Enabled:_u.Disabled,this.cu=t.OrgSettings,pe(null!==(i=this.cu.UrlPrivacyConfig)&&void 0!==i?i:cn.DefaultOrgSettings.UrlPrivacyConfig,this.cu.MaxUrlLength);var v=null!==(r=this.cu.AttributeBlocklist)&&void 0!==r?r:[];(null===(s=null===(e=this._u)||void 0===e?void 0:e.privacy)||void 0===s?void 0:s.attributeBlocklist)&&(this._u.privacy.attributeBlocklist.length,v.push.apply(v,this._u.privacy.attributeBlocklist.map(Ae))),xe(v),this.yu.consoleWatcher().initializeMaxLogsPerPage(this.cu.MaxConsoleLogPerPage),this.yu.ajaxWatcher().initialize({requests:this.cu.HttpRequestHeadersAllowlist,responses:this.cu.HttpResponseHeadersAllowlist,maxAjaxPayloadLength:this.cu.MaxAjaxPayloadLength}),this.yu.perfWatcher().initialize({resourceUploader:this.yu.getResourceUploader(),recTimings:!!this.cu.RecordPerformanceResourceTiming,recImgs:!!this.cu.RecordPerformanceResourceImg,maxPerfMarksPerPage:this.cu.MaxPerfMarksPerPage}),this.Xt.initialize({canvasWatcherMode:t.Flags.CanvasWatcherMode,blocks:t.ElementBlocks,deferreds:t.ElementDeferreds,keeps:t.ElementKeeps,watches:t.ElementWatches}),this.Ve.initialize(t.UrlKeeps),this.Xt.initializeConsent(null!=n?n:!!t.Consented),"number"==typeof t.BundleUploadInterval&&(this.fu=t.BundleUploadInterval),h&&this.enableResourceUploading(),u&&t.AjaxWatches&&this.yu.ajaxWatcher().setWatches(t.AjaxWatches),a&&this.At.enableEasyBake(),f&&(this.hu=!0),this.yu.start(t.Flags)},t.prototype.fullyStarted=function(){this.Au&&this.Au()},t.prototype.enableResourceUploading=function(){this.wu=!0,this.yu.initResourceUploading()},t.prototype.flushPendingChildFrameInits=function(){if(this.du.length>0){for(var t=0;t0&&this.At.sendEvents(e,i);break;case du.RequestFrameId:if(!t)return;var s=this.Nu(t);void 0===s||(this.mu[s]=!1,this.Lu(t,s));case du.Unknown:}},t.prototype.Nu=function(t){for(var n=0,i=this.vu;n2e6))try{localStorage._fs_swan_song=i}catch(t){}},t.prototype.sing=function(){try{var t=this.purge();if(void 0===t)return;if(!(t.Bundles&&t.UserId&&t.SessionId&&t.PageId))return;t.OrgId||(t.OrgId=this.Uu.getOrgId()),t.Bundles.length>0&&(t.Bundles.length,this.Du(t))}catch(t){}},t.prototype.purge=function(){try{if("_fs_swan_song"in localStorage){var t=localStorage._fs_swan_song;return delete localStorage._fs_swan_song,pt(t)}}catch(t){}},t.prototype.Du=function(t,n){return void 0===n&&(n=0),(0,e.__awaiter)(this,void 0,Yn,function(){var i,r,s,o;return(0,e.__generator)(this,function(u){switch(u.label){case 0:if(i=null,!tt(t.Bundles)||0===t.Bundles.length||void 0===t.Bundles[0])return[2];1==t.Bundles.length&&(i=this._t.time.wallTime()-(t.LastBundleTime||0)),u.label=1;case 1:return u.trys.push([1,3,,4]),[4,this.Ho.bundle({bundle:t.Bundles[0],deltaT:i,isNewSession:t.IsNewSession,orgId:t.OrgId,pageId:t.PageId,recHost:t.RecHost,serverBundleTime:t.ServerBundleTime,serverPageStart:t.ServerPageStart,sessionId:t.SessionId,userId:t.UserId,version:t.Version})];case 2:return r=u.sent(),s=r[1],t.Bundles[0].Evts.length,t.Bundles[0].Seq,t.Bundles.shift(),t.Bundles.length>0&&this.Du((0,e.__assign)((0,e.__assign)({},t),{ServerBundleTime:s.BundleTime})),[3,4];case 3:return(o=u.sent())instanceof iu&&ru(o.status)?[2]:(this.Bu=new this.Fu(this.Du,this.Ho.exponentialBackoffMs(n,!0),this,t,n+1).start(),[3,4]);case 4:return[2];}})})},t}(),ju=function(){function t(){}return t.prototype.encode=function(t){return t},t}(),Ou=function(){function t(){this.dict={idx:-1,map:{}},this.nodeCount=1,this.startIdx=0}return t.prototype.encode=function(n){if(0==n.length)return[];var i,r,e=n[0],s=Object.prototype.hasOwnProperty.call(this.dict.map,e)?this.dict.map[e]:void 0,o=[],u=1;function a(){s?u>1?o.push([s.idx,u]):o.push(s.idx):o.push(e)}for(i=1;ithis._t.recording.bundleUploadInterval()?[4,this.aa()]:[3,4]):[2];case 3:e.sent(),e.label=4;case 4:return[3,6];case 5:if((r=e.sent())instanceof iu){if(ru(r.status))return 206==r.status?Tt.sendToBugsnag("Failed to send bundle, probably because of its large size","error"):r.status>=500&&Tt.sendToBugsnag("Failed to send bundle, recording outage likely","error"),this.ea&&this.ea(),[2]}else Tt.sendToBugsnag("Failed to send bundle, unknown err","error",{err:r});return this.qu=!0,this.Vu=this.$u+this.Ho.exponentialBackoffMs(this.Qu++,!1),[3,6];case 6:return[2];}})})},t.prototype.va=function(t){var n,i;return(0,e.__awaiter)(this,void 0,Yn,function(){var r,s,o,u;return(0,e.__generator)(this,function(e){switch(e.label){case 0:return this.Ou?(window,r=this.co.getMsSinceLastUserAcivity(),[4,this.Ho.bundle({bundle:t,deltaT:null,lastUserActivity:r,orgId:this.Uu.getOrgId(),pageId:this.Ou,serverBundleTime:this.Zu,serverPageStart:this.Mu,isNewSession:this.Gu,sessionId:null!==(n=this.Uu.getSessionId())&&void 0!==n?n:"",userId:this.Uu.getUserId(),version:this._t.recording.bundleApiVersion()})]):[2];case 1:return s=e.sent(),o=s[0],u=s[1],null===(i=this._t.recording.observer)||void 0===i||i.onBundleSent(o),o>this.Ju&&this.zu>16&&this._t.recording.splitPage(Wt.Size),window,[2,u];}})})},t.prototype.fa=function(t){if(0===t.Evts.length)return t;for(var n=[],i=0,r=t.Evts;i0},t.prototype.hasActiveEvents=function(){return this.da},t.prototype.pushEvent=function(t){Mu[t.Kind]||(this.da=!0),this.pa.When<0&&(this.pa.When=t.When),this.pa.Evts.push(t)},t}();function Hu(t,n){void 0===t&&(t=[]),void 0===n&&(n=0);for(var i="",r=0,e=t;r-1},t.prototype.ba=function(){return this.Dn.document.location.search.indexOf("_fs_preview=false")>-1},t.prototype.ya=function(){return!!this.wa.getValue(this.ga)},t}();function Uu(t){var n,i,r;return{Kind:Ot.CAPTURE_SOURCE,Args:[t.type,t.entrypoint,"dom",null===(i=null===(n=t.source)||void 0===n?void 0:n.integration)||void 0===i?void 0:i.slice(0,1024),!!(null===(r=t.source)||void 0===r?void 0:r.userInitiated)]}}function Fu(t){return(0,e.__awaiter)(this,void 0,Yn,function(){var n,i,r,s;return(0,e.__generator)(this,function(e){if(n=function(t){return"msCrypto"in t?t.msCrypto:t.crypto}(t),"function"==typeof(null==n?void 0:n.randomUUID))return[2,n.randomUUID()];for(i=new Uint8Array(16),n.getRandomValues(i),i[6]=15&i[6]|64,i[8]=63&i[8]|128,r=[],s=0;s=864e5)return Wu;var c=null!==(n=this.Sa.getLastUserActivityTimeMS())&&void 0!==n?n:u;return o.mathAbs(s-c)>=qu||(null!==(i=this.Sa.getPageCount())&&void 0!==i?i:0)>=250?Wu:e},t.prototype.start=function(){this.lastUserActivityTimeout.start(3e5)},t.prototype.stop=function(){this.lastUserActivityTimeout.stop()},t.prototype.ka=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){var t;return(0,e.__generator)(this,function(n){return(t=this.Sa.getUserId())&&Bu(t)?[2,t]:[2,Fu(this._t.window)]})})},t.prototype.xa=function(){var t=this.co.getLastUserAcitivityTS();t!==this.lastUserActivityTS&&(this.lastUserActivityTS=t,this.Sa.setLastUserActivityTimeMS(t),this.start())},t}(),Vu=function(t){function n(n,i,r,e,s,o,u){void 0===r&&(r=!0),void 0===e&&(e=new Fo(n)),void 0===s&&(s=new Ku(n,i,e,r)),void 0===o&&(o=Zi),void 0===u&&(u=xu);var a,c=t.call(this,n,o,e,s,u)||this;return c.Ho=i,c.mo=s,c._a=!1,c.ko=!1,c.Aa=!1,s.onShutdown(function(){return c.shutdown(Wt.SettingsBlocked)}),c.Mt=c.Dn.document,c.Eo=0,c.Uu=n.recording.identity,c.Ia=new Lu(c.xu,c.Dn,c.Uu.getClientStore()),c.pu=_u.NoInfoYet,c.Ta=new Qu(n,e,c.Uu),a=function(t){if(c.yu.stop(Wt.Api),t){var n=c.Mt.getElementById(t);n&&c.Ca&&n.setAttribute("_fs_embed_token",c.Ca)}},c.Dn._fs_shutdown=a,c}return(0,e.__extends)(n,t),n.prototype.onDomLoad=function(){var n=this;t.prototype.onDomLoad.call(this),this._a=!0,this.Pa(function(){n.fireFsReady(n.ko)})},n.prototype.ja=function(){var t=R(this.Dn,"_fs_replay_flags");if(/[?&]_fs_force_session=true(&|#|$)/.test(location.search)&&(t+=",forceSession",this.Dn.history)){var n=location.search.replace(/(^\?|&)_fs_force_session=true(&|$)/,function(t,n,i){return i?n:""});this.Dn.history.replaceState({},"",this.Dn.location.href.replace(location.search,n))}return t},n.prototype.start=function(n,i,r){var s,o,u;return(0,e.__awaiter)(this,void 0,Yn,function(){var a,c,h,f,v,l,d,p,w,g,m,y,b,E,S,x,k,_,A,I,T,C,P,j=this;return(0,e.__generator)(this,function(e){switch(e.label){case 0:t.prototype.start.call(this,n,i,r),a=this.ja(),c=yi(this.Mt),h=c[0],f=c[1],O=this.Dn,M=0,K=0,v=null==O.screen?[M,K]:(M=parseInt(String(O.screen.width),10),K=parseInt(String(O.screen.height),10),[M=isNaN(M)?0:M,K=isNaN(K)?0:K]),l=v[0],d=v[1],p="",n||(p=this.Uu.getUserId()),w=null!==(u=null===(o=null===(s=this._t)||void 0===s?void 0:s.recording)||void 0===o?void 0:o.preroll)&&void 0!==u?u:-1,g=function(){return he(Mr(j.Dn),{source:"page",type:"base"})},m=function(){return he(j.Dn.location.href,{source:"page",type:"url"})},y=function(){return""===j.Mt.referrer?"":he(j.Mt.referrer,{source:"page",type:"referrer"})},b=function(t){var n,i="_fs_tab_id";try{var r=t.sessionStorage.getItem(i);if(r)return r;var e=Math.floor(1e17*Math.random()).toString(16);return t.sessionStorage.setItem(i,e),null!==(n=t.sessionStorage.getItem(i))&&void 0!==n?n:void 0}catch(t){return}}(this.Dn),E={OrgId:this.xu,UserId:p,Url:m(),Base:g(),Width:h,Height:f,ScreenWidth:l,ScreenHeight:d,SnippetVersion:V(this.Dn),Referrer:y(),Preroll:w,Doctype:dt(this.Mt),CompiledVersion:"11aa377d19",CompiledTimestamp:1678707725,AppId:this.Uu.getAppId(),TabId:b,PreviewMode:this.Ia.isPreviewMode()||void 0},a&&(E.ReplayFlags=a),e.label=1;case 1:return e.trys.push([1,5,,6]),S=this.Oa,[4,this.Ho.page(E)];case 2:return[4,S.apply(this,[e.sent()])];case 3:return P=e.sent(),this.isSafeResponse(P)?this.gu?[2]:(window,this.handleResponse(P),window,this.Ma(P.CookieDomain,P.UserIntId,P.SessionIntId,P.PageIntId,P.EmbedToken),P.Flags.UseStatelessConsent||this.Uu.getConsentStore().setConsentState(!!P.Consented),this.Ka(),P.PreviewMode&&this.Ra(),x=function(t){return R(t,"_fs_pagestart","function")}(this.Dn),x&&x(),this.At.enqueueFirst(this.yu.getNavigateEvent(this.Dn.location.href,Ot.ENTRY_NAVIGATE)),k=!!P.Consented,this.At.enqueueFirst({Kind:Ot.SYS_REPORTCONSENT,Args:[k,Bt.Document]}),_=dt(this.Mt),A=m(),I=y(),T=g(),this.At.enqueueFirst({Kind:Ot.SET_FRAME_BASE,Args:[he(Mr(this.Dn),{source:"event",type:Ot.SET_FRAME_BASE}),_,A,I]}),this.mo.setPageData({Kind:Ot.PAGE_DATA,Args:[A,T,h,f,l,d,V(this.Dn),I,_,w,p,P.PageStart,Et(this.Dn),this.Dn.navigator.userAgent,b,!!P.IsNewSession]}),this.At.enqueue({Kind:Ot.SCRIPT_COMPILED_VERSION,Args:["11aa377d19"]}),this.At.enqueue(Uu({type:"default"})),this.yu.addVisibilityChangeEvent(),this.addInitEvent(),[4,this.At.startPipeline({pageId:P.PageIntId,serverPageStart:P.PageStart,isNewSession:!!P.IsNewSession})]):[2,this.Ha()];case 4:return e.sent(),this.enqueueDocumentProperties(this.Mt),this.fullyStarted(),[3,6];case 5:return(C=e.sent())instanceof iu&&(P=C.data)&&P.user_id&&P.cookie_domain&&P.reason_code===en.ReasonBlockedTrafficRamping&&p!==P.user_id&&this.Ma(P.cookie_domain,P.user_id,"","",""),this.Ha(),[3,6];case 6:return[2];}var O,M,K})})},n.prototype.Ka=function(){var t=this;this.Aa=!0,this.Pa(function(){t.fireFsReady(t.ko)})},n.prototype.Ma=function(t,n,i,r,e){var s=this.Uu;s.setIds(this.Dn,t,n,i),this.Ca=e,this.Ia.write(),s.getUserId(),s.getSessionId()},n.prototype.Pa=function(t){var n,i;if(this._a&&this.Aa)if(null===(i=null===(n=this.ku)||void 0===n?void 0:n.Flags)||void 0===i?void 0:i.FetchIntegrations){var r=this.Mt.createElement("script");r.addEventListener("load",t),r.addEventListener("error",t),r.async=!0,r.src=this.zo+"//"+this.Eu+"/rec/integrations?OrgId="+this.xu,this.Mt.head.appendChild(r)}else t()},n.prototype.Ra=function(){var t="FullStory-preview-script";if(!this.Mt.getElementById(t)){var n=this.Mt.createElement("script");n.id=t,n.async=!0,n.src=this.zo+"//"+this.Su+"/s/fspreview.js",this.Mt.head.appendChild(n)}},n.prototype.Ha=function(){this.Iu&&this.Iu(),this.shutdown(Wt.SettingsBlocked),this.ko=!0,this.fireFsReady(this.ko)},n.prototype.Oa=function(t){var n;return(0,e.__awaiter)(this,void 0,Yn,function(){var i,r,s,o,u;return(0,e.__generator)(this,function(a){switch(a.label){case 0:return(i=(0,e.__assign)({},t)).Flags.UseStaticSettings?(r=this.Ia.isPreviewMode(),[4,this.Ho.settings({orgId:this.xu,previewMode:r,fallback:!1})["catch"](function(t){Tt.sendToBugsnag("Edge Rec settings error","error",{err:t})})]):[3,4];case 1:return(s=a.sent())?[3,3]:[4,this.Ho.settings({orgId:this.xu,previewMode:r,fallback:!0})["catch"](function(t){Tt.sendToBugsnag("Rs Rec settings error","error",{err:t})})];case 2:s=a.sent(),a.label=3;case 3:s&&(i=(0,e.__assign)((0,e.__assign)({},i),s)),a.label=4;case 4:return i.Flags.UseClientSideId?(this.Uu.setCookieDomain(this.Dn,i.CookieDomain),Bu(o=null!==(n=t.UserUUID)&&void 0!==n?n:"")&&this.Uu.setUserId(o),[4,this.Ta.createUserSessionPage()]):[3,6];case 5:u=a.sent(),this.Ta.start(),i=(0,e.__assign)((0,e.__assign)({},i),{UserIntId:u.userId,SessionIntId:u.sessionId,PageIntId:u.pageId,IsNewSession:u.isNewSession,PageStart:p()}),a.label=6;case 6:return i.Flags.UseStatelessConsent&&(i=(0,e.__assign)((0,e.__assign)({},i),{Consented:this.Uu.getConsentStore().getConsentState()})),[2,i];}})})},n.prototype.onMessageReceived=function(n,i){t.prototype.onMessageReceived.call(this,n,i),(null==n?void 0:n.parent)==this.Dn&&i[0]===du.EndPreviewMode&&this.Ia.clear()},n}(Cu),zu=function(){function t(t,n){void 0===n&&(n=new $u(t)),this.Dn=t,this.Na=n}return t.prototype.enqueueEvents=function(t,n){var i=null!=t?t:void 0;this.Na.postMessage(this.Dn.parent,[du.EvtBundle,n,i],i)},t.prototype.startPipeline=function(){},t.prototype.stopPipeline=function(){},t.prototype.flush=function(){return(0,e.__awaiter)(this,void 0,Yn,function(){return(0,e.__generator)(this,function(t){return[2]})})},t.prototype.singSwanSong=function(){},t.prototype.onShutdown=function(t){},t.prototype.setPageData=function(t){},t}(),$u=function(){function t(t){this.Dn=t}return t.prototype.postMessage=function(t,n,i){switch(n[0]){case du.EvtBundle:bu(this.Dn,n[0],vt(n[1]),i)||mu(t,n);break;case du.RequestFrameId:bu(this.Dn,n[0],"[]",i)||mu(t,n);break;default:n[0];}},t}(),Gu=function(t){function n(n,i,r,e,s){void 0===i&&(i=new $u(n.window)),void 0===r&&(r=new zu(n.window,i)),void 0===e&&(e=Zi),void 0===s&&(s=xu);var o=t.call(this,n,e,void 0,r,s)||this;return o.Na=i,o}return(0,e.__extends)(n,t),n.prototype.start=function(n,i,r){var e=this;t.prototype.start.call(this,n,i,r),this.La(),this.T.add(this.Dn,"load",!1,function(){e.yu.recordingIsDetached()&&e._t.recording.splitPage(Wt.FsShutdownFrame)}),this.yu.addVisibilityChangeEvent()},n.prototype.onMessageReceived=function(n,i){if(t.prototype.onMessageReceived.call(this,n,i),n===this.Dn.parent||n===this.Dn)switch(i[0]){case du.GreetFrame:this.La(i[1]);break;case du.SetFrameId:try{var r=i[1];if(!r)return void he(location.href,{source:"log",type:"debug"});this.Ua({frameId:r,parentIds:i[2],outerStartTime:i[3],scheme:i[4],script:i[5],appHost:i[6],orgId:i[7],initConfig:i[8],pageRsp:i[9],consentOverride:i[10],minimumWhen:i[11]})}catch(t){vt(i)}break;case du.SetConsent:this.setConsent(i[1]);break;case du.InitFrameMobile:try{var e=JSON.parse(i[1]),s=e.StartTime;if(i.length>2&&i[2]){var o=i[2];Object.prototype.hasOwnProperty.call(o,"ProtocolVersion")&&o.ProtocolVersion>=20180723&&Object.prototype.hasOwnProperty.call(o,"OuterStartTime")&&(s=o.OuterStartTime)}var u=e.Host;this.Ua({frameId:0,parentIds:[],outerStartTime:s,scheme:"https:",script:G(u),appHost:$(u),orgId:e.OrgId,initConfig:void 0,pageRsp:e.PageResponse,consentOverride:this.Xt.getConsent()})}catch(t){vt(i)}}},n.prototype.La=function(t){this.Eo&&this.Eo===t||0!=this.Eo&&this.Dn.parent&&this.Na.postMessage(this.Dn.parent,[du.RequestFrameId])},n.prototype.Ua=function(t){var n,i,r=this;if(this.Eo)this.Eo!==t.frameId?(this.Eo,t.frameId,this._t.recording.splitPage(Wt.FsShutdownFrame)):this.Eo;else if(he(location.href,{source:"log",type:"debug"}),t.frameId,this.zo=t.scheme,this.bu=t.script,this.Su=t.appHost,this.xu=t.orgId,this._u=t.initConfig,this.Eo=t.frameId,this.So=t.parentIds,t.pageRsp&&this.isSafeResponse(t.pageRsp)){if(!this.gu){var e=null!==(n=t.consentOverride)&&void 0!==n?n:!!t.pageRsp.Consented;this.handleResponse(t.pageRsp,e),this.fireFsReady(),this.At.enqueueFirst({Kind:Ot.SYS_REPORTCONSENT,Args:[e,Bt.Document]}),this.At.enqueueFirst({Kind:Ot.SET_FRAME_BASE,Args:[he(Mr(this.Dn),{source:"event",type:Ot.SET_FRAME_BASE}),dt(this.Dn.document)]}),this.At.enqueue({Kind:Ot.SCRIPT_COMPILED_VERSION,Args:["11aa377d19"]}),this.At.enqueue(Uu({type:"default"})),this.addInitEvent(),this.At.rebaseIframe(t.outerStartTime,null!==(i=t.minimumWhen)&&void 0!==i?i:0),this._t.time.setStartTime(t.outerStartTime),this.Ou&&this.At.startPipeline({pageId:this.Ou,serverPageStart:t.pageRsp.PageStart,isNewSession:!!t.pageRsp.IsNewSession,frameId:t.frameId,parentIds:t.parentIds}).then(function(){r.flushPendingChildFrameInits(),r.enqueueDocumentProperties(r.Dn.document),r.fullyStarted()})}}else this.shutdown(Wt.FsShutdownFrame)},n}(Cu),Xu=function(){function t(t,n,i){void 0===n&&(n=function(){}),void 0===i&&(i=!1),this.Mt=t,this.Fa=n,this.Da=i,this._cookies={},this._cookies=k(this.Mt)}return t.prototype.setDomain=function(t){this.Ba=t},t.prototype.getValue=function(t,n){var i=this._cookies[t];if(!i)try{i=localStorage[null!=n?n:t]}catch(t){}return i},t.prototype.setValue=function(t,n,i,r){if(null!=this.Ba&&!this.Da){var e=[];this._setCookie(t,n,i,e),this.Wa(null!=r?r:t,n,e,t),e.length>0&&this.Fa(e)}},t.prototype.setCookie=function(t,n,i){this._setCookie(t,n,i,[])},Object.defineProperty(t.prototype,"cookies",{get:function(){return this._cookies},enumerable:!1,configurable:!0}),t.prototype.clearCookie=function(t,n){if(this._cookies[t]&&(this.Mt.cookie=Ju(this.Ba,t,"","Thu, 01 Jan 1970 00:00:01 GMT"),delete this._cookies[t]),n)try{delete localStorage[n]}catch(t){}},t.prototype._setCookie=function(t,n,i,r){try{this.Mt.cookie=Ju(this.Ba,t,n,i),-1===this.Mt.cookie.indexOf(n)&&r.push([t,"cookie"])}finally{this._cookies=k(this.Mt)}},t.prototype.Wa=function(t,n,i,r){try{localStorage[t]=n,localStorage[t]!==n&&i.push([null!=r?r:t,"localStorage"])}catch(n){i.push([null!=r?r:t,"localStorage",String(n)])}},t}();function Ju(t,n,i,r){var e=n+"="+i;return e+="; domain="+function(t){return t?"."+encodeURIComponent(t):""}(t),e+="; Expires="+r+"; path=/; SameSite=Strict","https:"===location.protocol&&(e+="; Secure"),e}var Zu,Yu="fs_cid",ta=function(){function t(t){this.Sa=t,this.qa=1;var n=this.Sa.getValue(Yu,wn);this.Qa=function(t){var n={consent:Dt.RevokeConsent};if(!t)return n;var i=t.split(".");return i.length<1?n:(i[0],"1"===i[1]?{consent:Dt.GrantConsent}:n)}(n)}return t.prototype.getConsentState=function(){return this.Qa.consent},t.prototype.setConsentState=function(t){if(this.Qa.consent=t,t!==Dt.RevokeConsent){var n=this.Va(),i=this.za();this.Sa.setValue(Yu,n,i,wn)}else this.Sa.clearCookie(Yu,wn)},t.prototype.Va=function(){return[this.qa,this.Qa.consent===Dt.GrantConsent?1:0].join(".")},t.prototype.za=function(){return new Date(1e3*S()).toUTCString()},t}(),na="fs_lua",ia=function(){function t(t){this.qa=1,this.Sa=t;var n=this.Sa.getValue(na,gn);this.Qa=function(t){var n={lastUserActivityTime:void 0};if(!t)return n;var i=t.split(".");return i.length<1?n:(i[0],{lastUserActivityTime:_(i[1])})}(n)}return t.prototype.getLastUserActivityTimeMS=function(){return this.Qa.lastUserActivityTime},t.prototype.setLastUserActivityTimeMS=function(t){this.Qa.lastUserActivityTime=t;var n=this.Va(),i=this.za();this.Sa.setValue(na,n,i,gn)},t.prototype.Va=function(){var t;return[this.qa,null!==(t=this.Qa.lastUserActivityTime)&&void 0!==t?t:""].join(".")},t.prototype.za=function(){return new Date(p()+qu).toUTCString()},t}(),ra="fs_uid",ea=function(){function t(t,n,i,r){void 0===n&&(n=document),void 0===i&&(i=function(){}),void 0===r&&(r=!1),this.$a=void 0,this.wa=new Xu(n,i,r),this.Ga=new ta(this.wa),this.Xa=new ia(this.wa),this.Qa=this.Ja(t)}return t.prototype.Ja=function(t){var n=x(this.wa.getValue(ra,pn));return n&&n.orgId==t?n:{expirationAbsTimeSeconds:S(),orgId:t,userId:"",sessionId:"",appKeyHash:""}},t.prototype.getConsentStore=function(){return this.Ga},t.prototype.clear=function(){this.Xa.setLastUserActivityTimeMS(void 0),this.Qa.sessionStartTime=this.Qa.pageCount=void 0,this.Qa.userId=this.Qa.sessionId=this.Qa.appKeyHash=this.$a="",this.Qa.expirationAbsTimeSeconds=S(),this.Za()},t.prototype.create=function(t){this.Xa.setLastUserActivityTimeMS(t.lastUserActivityTime),this.Qa=(0,e.__assign)((0,e.__assign)({},this.Qa),t),this.Za()},t.prototype.getOrgId=function(){return this.Qa.orgId},t.prototype.getUserId=function(){return this.Qa.userId},t.prototype.setUserId=function(t){this.Qa.userId=t,this.Za()},t.prototype.getSessionId=function(){return this.Qa.sessionId},t.prototype.getAppKeyHash=function(){return this.Qa.appKeyHash},t.prototype.getCookies=function(){return this.wa.cookies},t.prototype.setAppId=function(t){this.$a=t,this.Qa.appKeyHash=qo(t),this.Za()},t.prototype.getAppId=function(){return this.$a},t.prototype.setSessionStartTimeMS=function(t){this.Qa.sessionStartTime=t,this.Za()},t.prototype.getSessionStartTimeMS=function(){return this.Qa.sessionStartTime},t.prototype.setLastUserActivityTimeMS=function(t){this.Xa.setLastUserActivityTimeMS(t)},t.prototype.getLastUserActivityTimeMS=function(){return this.Xa.getLastUserActivityTimeMS()},t.prototype.setPageCount=function(t){this.Qa.pageCount=t,this.Za()},t.prototype.getPageCount=function(){return this.Qa.pageCount},t.prototype.getClientStore=function(){return this.wa},t.prototype.setCookie=function(t,n,i){void 0===i&&(i=new Date(p()+6048e5).toUTCString()),this.wa.setCookie(t,n,i)},t.prototype.setCookieDomain=function(t,n){var i=n;(C(i)||i.match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/g))&&(i="");var r=function(t){return R(t,"_fs_cookie_domain")}(t);"string"==typeof r&&(i=r),this.wa.setDomain(i)},t.prototype.setIds=function(t,n,i,r){this.setCookieDomain(t,n),this.Qa.userId=i,this.Qa.sessionId=r,this.Za()},t.prototype.clearAppId=function(){return!!this.Qa.appKeyHash&&(this.$a="",this.Qa.appKeyHash="",this.Za(),!0)},t.prototype.encode=function(){var t,n,i,r=[this.Qa.userId,null!==(t=this.Qa.sessionId)&&void 0!==t?t:"",""+(null!==(n=this.Qa.sessionStartTime)&&void 0!==n?n:""),"",""+(null!==(i=this.Qa.pageCount)&&void 0!==i?i:"")].join(":"),e=["",this.Qa.orgId,r];return this.Qa.appKeyHash&&e.push(encodeURIComponent(this.Qa.appKeyHash)),e.push("/"+this.Qa.expirationAbsTimeSeconds),e.join("#")},t.prototype.Za=function(){var t=this.encode(),n=new Date(1e3*this.Qa.expirationAbsTimeSeconds).toUTCString();this.wa.setValue(ra,t,n,pn)},t}(),sa=((Zu={})[Xt.Document]={assetMapId:"str",releaseDatetime:"date",releaseVersion:"str"},Zu[Xt.Event]={},Zu[Xt.Page]={pageName:"str",releaseVersion:"str",releaseDatetime:"str"},Zu[Xt.User]={uid:"str",displayName:"str",email:"str"},Zu),oa={str:ua,bool:aa,real:ca,"int":ha,date:fa,strs:va(ua),bools:va(aa),reals:va(ca),ints:va(ha),dates:va(fa),objs:va(la),obj:la};function ua(t){return"string"==typeof t}function aa(t){return"boolean"==typeof t}function ca(t){return"number"==typeof t}function ha(t){return"number"==typeof t&&t-o.mathFloor(t)==0}function fa(t){return!(!t||(t.constructor===Date?isNaN(t):"number"!=typeof t&&"string"!=typeof t||isNaN(new Date(t))))}function va(t){return function(n){if(!(n instanceof Array))return!1;for(var i=0;i=0)return[void 0,Jt.FsId];var e=qo(r),s=void 0;return n&&n.Qa.appKeyHash&&n.Qa.appKeyHash!==e&&n.Qa.appKeyHash!==r&&(n.Qa.appKeyHash,s=Jt.NewUid),[r,s]}(f,this.Uu),l=v[0],d=v[1];if(!l)return Jt.FsId,{events:r};a.properties.uid=l,this.Uu.setAppId(l),d===Jt.NewUid&&(i=!0)}}Ea(t.source,"setVars",e),e(this.nc(s,wa(s,a.properties),u));break;default:(0,Ir.nt)(s,"Unsupported");}}catch(n){t.operation,n.message}return{events:r,reidentify:i}},t.prototype.nc=function(t,n,i,r){var e=vt(n.PayloadToSend),s=!!i&&"fs"!==i;switch(t){case Xt.Event:return{When:0,Kind:Ot.SYS_CUSTOM,Args:s?[r,e,i]:[r,e]};case Xt.Document:case Xt.Page:case Xt.User:return{When:0,Kind:Ot.SYS_SETVAR,Args:s?[t,e,i]:[t,e]};default:(0,Ir.nt)(t,"Unsupported");}},t.prototype.ic=function(t,n){var i=t.PayloadToSend;if(i&&"object"==typeof i){var r=0,e={};for(var s in i)if(!(s in this.Ya)){var o=i[s];this.Ya[s]={value:o,apiSource:n},e[s]=o,r++}if(0!==r)return{PayloadToSend:e,ValidationErrors:t.ValidationErrors}}},t}();function wa(t,n){var i=1500;return ga(function(){return--i},t,n)}var ga=function(t,n,i){var r,e,s={PayloadToSend:{},ValidationErrors:[]},u=function(i){var r=ga(t,n,i);return s.ValidationErrors=s.ValidationErrors.concat(r.ValidationErrors),r.PayloadToSend};for(var a in i)if(o.objectHasOwnProp(i,a)){if(t()<=0)break;var c=i[a],h=ya(n,a,c,s.ValidationErrors);if(h){var f=h.name;if("obj"!==h.type){if("objs"!==h.type)s.PayloadToSend[f]=ma(h.type,h.value);else{n!=Xt.Event&&s.ValidationErrors.push({Type:"vartype",FieldName:f,ValueType:"Array (unsupported)"});for(var v=[],l=0;l0&&(s.PayloadToSend[f]=v)}}else{var d=u(h.value),p=(e="_obj").length>(r=a).length||r.substring(r.length-e.length)!=e?f.substring(0,f.length-"_obj".length):f;s.PayloadToSend[p]=d}}else s.PayloadToSend[a]=ma("str",c)}return s};function ma(t,n){var i=n;return"str"==t&&"string"==typeof i&&(i=i.trim()),null==i||"date"!=t&&i.constructor!=Date||(i=function(t){var n=t.constructor===Date?t:new Date(t);try{return n.toISOString()}catch(t){return null}}(i)),i}function ya(t,n,i,r){var e=n,s=e,u=typeof i;if("undefined"===u)return r.push({Type:"vartype",FieldName:e,ValueType:u+" (unsupported)"}),null;var a=sa[t];if(o.objectHasOwnProp(a,e))return{name:e,type:a[e],value:i};var c=e.lastIndexOf("_");if(-1==c||!ba(e.substring(c+1))){var h=function(t){for(var n in oa)if(oa[n](t))return n;return null}(i);if(null==h)return i?r.push({Type:"vartype",FieldName:e}):r.push({Type:"vartype",FieldName:e,ValueType:"null (unsupported)"}),null;c=e.length,e=e+"_"+h}var f=e.substring(0,c),v=e.substring(c+1);if("object"===u&&!i)return r.push({Type:"vartype",FieldName:s,ValueType:"null (unsupported)"}),null;if(!da.test(f)){f=f.replace(/[^a-zA-Z0-9_]/g,"").replace(/^[0-9]+/,""),/[0-9]/.test(f[0])&&(f=f.substring(1)),r.push({Type:"varname",FieldName:s});var l=f+"_"+v;if(da.source,""==f)return null;e=l}return ba(v)?function(t,n){return oa[t](n)}(v,i)?{name:e,type:v,value:i}:(vt(i),"number"===u?u=i%1==0?"integer":"real":"object"==u&&null!=i&&i.constructor==Date&&(u=isNaN(i)?"invalid date":"date"),r.push({Type:"vartype",FieldName:s,ValueType:u}),null):(r.push({Type:"varname",FieldName:s}),null)}function ba(t){return!!oa[t]}function Ea(t,n,i){var r=Uu({source:t,type:"api",entrypoint:n});r&&i({When:0,Kind:r.Kind,Args:r.Args})}function Sa(t,n){return(0,e.__awaiter)(this,void 0,Yn,function(){var i,s,o,a,c;return(0,e.__generator)(this,function(h){switch(h.label){case 0:if(h.trys.push([0,2,,3]),gr||yr||function(t){return!!R(t,"_fs_use_polyfilled_apis","boolean")}(t))return[2,(0,e.__assign)((0,e.__assign)({},n),{status:r.Clean})];if(!t.document||n.status!==r.Unknown)return[2,n];if(i=function(t,n){var i=n.functions,s={},o=(0,e.__assign)({},n.helpers);if(o.functionToString=function(t,n){var i,r,e=null===(i=t["__core-js_shared__"])||void 0===i?void 0:i.inspectSource;if(e){var s=function(){return e(this)};if(ka(s,2))return s}var o=null===(r=t["__core-js_shared__"])||void 0===r?void 0:r["native-function-to-string"];if(ka(o))return o;var u=n.__zone_symbol__OriginalDelegate;return ka(u)?u:ka(n)?n:void 0}(t,o.functionToString),!o.functionToString)return n;var u=!1;for(var a in i)if(i[a]){if(s[a]=Ia(o.functionToString,i[a]),s[a]||(s[a]=Ta(o.functionToString,o,a)),!s[a])return n;s[a]!==i[a]&&(u=!0)}else s[a]=void 0;return{status:r.Clean,functions:u?s:i,helpers:o,errors:[]}}(t,n),i.status===r.Clean)return[2,i];(s=t.document.createElement("iframe")).id="FullStory-iframe",s.className="fs-hide",s.style.display="none",o=t.document.body||t.document.head||t.document.documentElement||t.document;try{o.appendChild(s)}catch(t){return[2,(0,e.__assign)((0,e.__assign)({},n),{status:r.Clean})]}return s.contentWindow?(a=u(s.contentWindow,r.Clean),s.parentNode&&s.parentNode.removeChild(s),a.status===r.UnrecoverableFailure?[2,(0,e.__assign)((0,e.__assign)({},n),{status:r.Clean})]:[4,xa(a,n)]):[2,(0,e.__assign)((0,e.__assign)({},n),{status:r.Clean})];case 1:return[2,h.sent()];case 2:return c=h.sent(),Tt.sendToBugsnag(c,"error"),[2,(0,e.__assign)((0,e.__assign)({},n),{status:r.Clean})];case 3:return[2];}})})}function xa(t,n){var i,s=new Yn(function(t){return i=t});return setTimeout(function(){try{t.functions.jsonParse("[]").push(0)}catch(t){i((0,e.__assign)((0,e.__assign)({},n),{status:r.Clean}))}i(t)}),s}function ka(t,n){var i;if(void 0===n&&(n=0),!t)return!1;try{t.call(function(){})}catch(t){return!1}var r=function(t){try{return void t.call(null)}catch(t){return(t.stack||"").replace(/__fs_nomangle_check_stack(.|\n)*$/,"")}},e=void 0;0!==n&&"number"==typeof Error.stackTraceLimit&&(e=Error.stackTraceLimit,Error.stackTraceLimit=Number.POSITIVE_INFINITY);var s=[function(){throw new Error("")},t],o=function __fs_nomangle_check_stack(){return s.map(r)}(),u=o[0],a=o[1];if(void 0!==e&&(Error.stackTraceLimit=e),!u||!a)return!1;for(var c="\n".charCodeAt(0),h=u.length>a.length?a.length:u.length,f=1,v=f;v=0}var Aa=["__zone_symbol__OriginalDelegate","nr@original"];function Ia(t,n){if(n){for(var i=0,r=Aa;i0&&this.lc[t].some(function(t){return!t.disconnected})},t.prototype.takeRecords=function(t){var n,i=null!==(n=this.lc[t.type])&&void 0!==n?n:[];if(0!==i.length)for(var r=0,e=i;r-1,!!ja.userAgent.match("CriOS")||"Google Inc."===Oa&&!Ma&&!Ka),Fa=/Firefox/.test(window.navigator.userAgent);function Da(t){if(!Fa)return!1;var n=window.navigator.userAgent.match(/Firefox\/(\d+)/);return!(!n||!n[1])&&parseInt(n[1],10)0||null===H){n="Init config rejected: "+R.unrecoverable.join(",\n"),k(t,new Error(n));break}R.recoverable.length>0&&(n="Init config partially rejected: "+R.recoverable.join(",\n")),a=H,x(t);break;default:(0,Ir.nt)(t,"invalid operation");}}catch(n){Tt.sendToBugsnag(n,"error"),k(t,n)}},A=0,I=p;A=0;u--)(e=t[u])&&(o=(s<3?e(o):s>3?e(n,i,o):e(n,i))||o);return s>3&&o&&Object.defineProperty(n,i,o),o}function a(t,n){return function(i,r){n(i,r,t)}}function c(t,n){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,n)}function h(t,n,i,r){return new(i||(i=Promise))(function(e,s){function o(t){try{a(r.next(t))}catch(t){s(t)}}function u(t){try{a(r["throw"](t))}catch(t){s(t)}}function a(t){var n;t.done?e(t.value):(n=t.value,n instanceof i?n:new i(function(t){t(n)})).then(o,u)}a((r=r.apply(t,n||[])).next())})}function f(t,n){var i,r,e,s,o={label:0,sent:function(){if(1&e[0])throw e[1];return e[1]},trys:[],ops:[]};return s={next:u(0),"throw":u(1),"return":u(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function u(s){return function(u){return function(s){if(i)throw new TypeError("Generator is already executing.");for(;o;)try{if(i=1,r&&(e=2&s[0]?r["return"]:s[0]?r["throw"]||((e=r["return"])&&e.call(r),0):r.next)&&!(e=e.call(r,s[1])).done)return e;switch(r=0,e&&(s=[2&s[0],e.value]),s[0]){case 0:case 1:e=s;break;case 4:return o.label++,{value:s[1],done:!1};case 5:o.label++,r=s[1],s=[0];continue;case 7:s=o.ops.pop(),o.trys.pop();continue;default:if(!((e=(e=o.trys).length>0&&e[e.length-1])||6!==s[0]&&2!==s[0])){o=0;continue}if(3===s[0]&&(!e||s[1]>e[0]&&s[1]=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}function p(t,n){var i="function"==typeof Symbol&&t[Symbol.iterator];if(!i)return t;var r,e,s=i.call(t),o=[];try{for(;(void 0===n||n-->0)&&!(r=s.next()).done;)o.push(r.value)}catch(t){e={error:t}}finally{try{r&&!r.done&&(i=s["return"])&&i.call(s)}finally{if(e)throw e.error}}return o}function w(){for(var t=[],n=0;n1||u(t,n)})})}function u(t,n){try{(i=e[t](n)).value instanceof y?Promise.resolve(i.value.v).then(a,c):h(s[0][2],i)}catch(t){h(s[0][3],t)}var i}function a(t){u("next",t)}function c(t){u("throw",t)}function h(t,n){t(n),s.shift(),s.length&&u(s[0][0],s[0][1])}}function E(t){var n,i;return n={},r("next"),r("throw",function(t){throw t}),r("return"),n[Symbol.iterator]=function(){return this},n;function r(r,e){n[r]=t[r]?function(n){return(i=!i)?{value:y(t[r](n)),done:"return"===r}:e?e(n):n}:e}}function S(t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var n,i=t[Symbol.asyncIterator];return i?i.call(t):(t=d(t),n={},r("next"),r("throw"),r("return"),n[Symbol.asyncIterator]=function(){return this},n);function r(i){n[i]=t[i]&&function(n){return new Promise(function(r,e){!function(t,n,i,r){Promise.resolve(r).then(function(n){t({value:n,done:i})},n)}(r,e,(n=t[i](n)).done,n.value)})}}}function x(t,n){return Object.defineProperty?Object.defineProperty(t,"raw",{value:n}):t.raw=n,t}var k=Object.create?function(t,n){Object.defineProperty(t,"default",{enumerable:!0,value:n})}:function(t,n){t["default"]=n};function _(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var i in t)"default"!==i&&Object.prototype.hasOwnProperty.call(t,i)&&v(n,t,i);return k(n,t),n}function A(t){return t&&t.__esModule?t:{"default":t}}function I(t,n,i,r){if("a"===i&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?t!==n||!r:!n.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?r:"a"===i?r.call(t):r?r.value:n.get(t)}function T(t,n,i,r,e){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!e)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?t!==n||!e:!n.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?e.call(t,i):e?e.value=i:n.set(t,i),i}function C(t,n){if(null===n||"object"!=typeof n&&"function"!=typeof n)throw new TypeError("Cannot use 'in' operator on non-object");return"function"==typeof t?n===t:t.has(n)}}},n={};function i(r){var e=n[r];if(void 0!==e)return e.exports;var s=n[r]={exports:{}};return t[r](s,s.exports,i),s.exports}i.d=function(t,n){for(var r in n)i.o(n,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},i.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i(248)}(); From 75c978151a7da7a620f9b7fbec2cac1fb0d75aca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 07:16:49 -0400 Subject: [PATCH 012/100] Update dependency elastic-apm-node to ^3.44.1 (main) (#154971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [elastic-apm-node](https://togithub.com/elastic/apm-agent-nodejs) | [`^3.44.0` -> `^3.44.1`](https://renovatebot.com/diffs/npm/elastic-apm-node/3.44.1/3.44.1) | [![age](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.44.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.44.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.44.1/compatibility-slim/3.44.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.44.1/confidence-slim/3.44.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/elastic/kibana). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 460a4769ad09a..e439f886118ee 100644 --- a/package.json +++ b/package.json @@ -770,7 +770,7 @@ "deepmerge": "^4.2.2", "del": "^6.1.0", "elastic-apm-http-client": "^11.0.1", - "elastic-apm-node": "^3.44.0", + "elastic-apm-node": "^3.44.1", "email-addresses": "^5.0.0", "execa": "^4.0.2", "expiry-js": "0.1.7", diff --git a/yarn.lock b/yarn.lock index 66fb913bb59c3..2776f05861c0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14275,7 +14275,7 @@ elastic-apm-node@^3.38.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^3.44.0: +elastic-apm-node@^3.44.1: version "3.44.1" resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.44.1.tgz#8a6e88fa9f3401455dddb87f9d5559f6d894964d" integrity sha512-hSf+S6GtQ+Ff5ueyQ1n2BAgAdiDOZ+XmHKFurzpB5/rvRFz6JbXLq+pcI11HMlK3ImiMVG9LQxHWMN6gqkqzYQ== From b6d4090de3b946f6cea157ffa2cc6cd6b9528103 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Apr 2023 13:31:40 +0200 Subject: [PATCH 013/100] [Synthetics UI] update sync service endpoints (#155054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandro Fernández Gómez --- .../getting_started/getting_started_page.tsx | 4 + .../service_api_client.test.ts | 114 ++++++++++++++++-- .../synthetics_service/service_api_client.ts | 60 +++++---- .../synthetics_service/synthetics_service.ts | 2 +- 4 files changed, 149 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx index c63faf20904a4..f42b781089aea 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx @@ -29,6 +29,7 @@ import { setAddingNewPrivateLocation, getAgentPoliciesAction, selectAgentPolicies, + cleanMonitorListState, } from '../../state'; import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui'; import { PrivateLocation } from '../../../../../common/runtime_types'; @@ -46,6 +47,9 @@ export const GettingStartedPage = () => { if (canReadAgentPolicies) { dispatch(getAgentPoliciesAction.get()); } + return () => { + dispatch(cleanMonitorListState()); + }; }, [canReadAgentPolicies, dispatch]); useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts index b65d4a30e0b62..22ec8e8a3213e 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts @@ -177,7 +177,6 @@ describe('callAPI', () => { 1, { isEdit: undefined, - runOnce: undefined, monitors: testMonitors.filter((monitor: any) => monitor.locations.some((loc: any) => loc.id === 'us_central') ), @@ -192,7 +191,6 @@ describe('callAPI', () => { 2, { isEdit: undefined, - runOnce: undefined, monitors: testMonitors.filter((monitor: any) => monitor.locations.some((loc: any) => loc.id === 'us_central_qa') ), @@ -207,7 +205,6 @@ describe('callAPI', () => { 3, { isEdit: undefined, - runOnce: undefined, monitors: testMonitors.filter((monitor: any) => monitor.locations.some((loc: any) => loc.id === 'us_central_staging') ), @@ -284,15 +281,15 @@ describe('callAPI', () => { }); expect(logger.debug).toHaveBeenNthCalledWith( 2, - 'Successfully called service location https://service.dev with method POST with 4 monitors ' + 'Successfully called service location https://service.devundefined with method POST with 4 monitors' ); expect(logger.debug).toHaveBeenNthCalledWith( 4, - 'Successfully called service location https://qa.service.elstc.co with method POST with 4 monitors ' + 'Successfully called service location https://qa.service.elstc.coundefined with method POST with 4 monitors' ); expect(logger.debug).toHaveBeenNthCalledWith( 6, - 'Successfully called service location https://qa.service.stg.co with method POST with 1 monitors ' + 'Successfully called service location https://qa.service.stg.coundefined with method POST with 1 monitors' ); }); @@ -320,7 +317,6 @@ describe('callAPI', () => { coreStart: mockCoreStart, } as UptimeServerSetup ); - apiClient.locations = testLocations; const output = { hosts: ['https://localhost:9200'], api_key: '12345' }; @@ -354,6 +350,110 @@ describe('callAPI', () => { url: 'https://service.dev/monitors', }); }); + + it('Calls the `/run` endpoint when calling `runOnce`', async () => { + const axiosSpy = (axios as jest.MockedFunction).mockResolvedValue({ + status: 200, + statusText: 'ok', + headers: {}, + config: {}, + data: { allowed: true, signupUrl: 'http://localhost:666/example' }, + }); + + const apiClient = new ServiceAPIClient( + logger, + { + manifestUrl: 'http://localhost:8080/api/manifest', + tls: { certificate: 'test-certificate', key: 'test-key' } as any, + }, + { isDev: true, stackVersion: '8.7.0' } as UptimeServerSetup + ); + + apiClient.locations = testLocations; + + const output = { hosts: ['https://localhost:9200'], api_key: '12345' }; + + await apiClient.runOnce({ + monitors: testMonitors, + output, + licenseLevel: 'trial', + }); + + expect(axiosSpy).toHaveBeenNthCalledWith(1, { + data: { + monitors: request1, + is_edit: undefined, + output, + stack_version: '8.7.0', + license_level: 'trial', + }, + headers: { + 'x-kibana-version': '8.7.0', + }, + httpsAgent: expect.objectContaining({ + options: { + rejectUnauthorized: true, + path: null, + cert: 'test-certificate', + key: 'test-key', + }, + }), + method: 'POST', + url: 'https://service.dev/run', + }); + }); + + it('Calls the `/monitors/sync` endpoint when calling `syncMonitors`', async () => { + const axiosSpy = (axios as jest.MockedFunction).mockResolvedValue({ + status: 200, + statusText: 'ok', + headers: {}, + config: {}, + data: { allowed: true, signupUrl: 'http://localhost:666/example' }, + }); + + const apiClient = new ServiceAPIClient( + logger, + { + manifestUrl: 'http://localhost:8080/api/manifest', + tls: { certificate: 'test-certificate', key: 'test-key' } as any, + }, + { isDev: true, stackVersion: '8.7.0' } as UptimeServerSetup + ); + + apiClient.locations = testLocations; + + const output = { hosts: ['https://localhost:9200'], api_key: '12345' }; + + await apiClient.syncMonitors({ + monitors: testMonitors, + output, + licenseLevel: 'trial', + }); + + expect(axiosSpy).toHaveBeenNthCalledWith(1, { + data: { + monitors: request1, + is_edit: undefined, + output, + stack_version: '8.7.0', + license_level: 'trial', + }, + headers: { + 'x-kibana-version': '8.7.0', + }, + httpsAgent: expect.objectContaining({ + options: { + rejectUnauthorized: true, + path: null, + cert: 'test-certificate', + key: 'test-key', + }, + }), + method: 'PUT', + url: 'https://service.dev/monitors/sync', + }); + }); }); const testLocations: PublicLocations = [ diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index 362fffea3a5a8..bfc872f3680a8 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -25,7 +25,7 @@ export interface ServiceData { hosts: string[]; api_key: string; }; - runOnce?: boolean; + endpoint?: 'monitors' | 'runOnce' | 'sync'; isEdit?: boolean; licenseLevel: string; } @@ -81,7 +81,7 @@ export class ServiceAPIClient { } async post(data: ServiceData) { - return this.callAPI('PUT', data); + return this.callAPI('POST', data); } async put(data: ServiceData) { @@ -93,7 +93,11 @@ export class ServiceAPIClient { } async runOnce(data: ServiceData) { - return this.callAPI('POST', { ...data, runOnce: true }); + return this.callAPI('POST', { ...data, endpoint: 'runOnce' }); + } + + async syncMonitors(data: ServiceData) { + return this.callAPI('PUT', { ...data, endpoint: 'sync' }); } addVersionHeader(req: AxiosRequestConfig) { @@ -112,7 +116,7 @@ export class ServiceAPIClient { const url = this.locations[Math.floor(Math.random() * this.locations.length)].url; /* url is required for service locations, but omitted for private locations. - /* this.locations is only service locations */ + /* this.locations is only service locations */ const httpsAgent = this.getHttpsAgent(url); if (httpsAgent) { @@ -138,7 +142,7 @@ export class ServiceAPIClient { async callAPI( method: 'POST' | 'PUT' | 'DELETE', - { monitors: allMonitors, output, runOnce, isEdit, licenseLevel }: ServiceData + { monitors: allMonitors, output, endpoint, isEdit, licenseLevel }: ServiceData ) { if (this.username === TEST_SERVICE_USERNAME) { // we don't want to call service while local integration tests are running @@ -154,25 +158,24 @@ export class ServiceAPIClient { locations?.find((loc) => loc.id === id && loc.isServiceManaged) ); if (locMonitors.length > 0) { + const promise = this.callServiceEndpoint( + { monitors: locMonitors, isEdit, endpoint, output, licenseLevel }, + method, + url + ); promises.push( - rxjsFrom( - this.callServiceEndpoint( - { monitors: locMonitors, isEdit, runOnce, output, licenseLevel }, - method, - url - ) - ).pipe( + rxjsFrom(promise).pipe( tap((result) => { this.logger.debug(result.data); this.logger.debug( - `Successfully called service location ${url} with method ${method} with ${locMonitors.length} monitors ` + `Successfully called service location ${url}${result.request?.path} with method ${method} with ${locMonitors.length} monitors` ); }), catchError((err: AxiosError<{ reason: string; status: number }>) => { pushErrors.push({ locationId: id, error: err.response?.data! }); const reason = err.response?.data?.reason ?? ''; - err.message = `Failed to call service location ${url} with method ${method} with ${locMonitors.length} monitors: ${err.message}, ${reason}`; + err.message = `Failed to call service location ${url}${err.request?.path} with method ${method} with ${locMonitors.length} monitors: ${err.message}, ${reason}`; this.logger.error(err); sendErrorTelemetryEvents(this.logger, this.server.telemetry, { reason: err.response?.data?.reason, @@ -197,19 +200,34 @@ export class ServiceAPIClient { } async callServiceEndpoint( - { monitors, output, runOnce, isEdit, licenseLevel }: ServiceData, + { monitors, output, endpoint = 'monitors', isEdit, licenseLevel }: ServiceData, method: 'POST' | 'PUT' | 'DELETE', - url: string + baseUrl: string ) { // don't need to pass locations to heartbeat const monitorsStreams = monitors.map(({ locations, ...rest }) => convertToDataStreamFormat(rest) ); + let url = baseUrl; + switch (endpoint) { + case 'monitors': + url += '/monitors'; + break; + case 'runOnce': + url += '/run'; + break; + case 'sync': + url += '/monitors/sync'; + break; + } + + const authHeader = this.authorization ? { Authorization: this.authorization } : undefined; + return axios( this.addVersionHeader({ method, - url: url + (runOnce ? '/run' : '/monitors'), + url, data: { monitors: monitorsStreams, output, @@ -217,12 +235,8 @@ export class ServiceAPIClient { is_edit: isEdit, license_level: licenseLevel, }, - headers: this.authorization - ? { - Authorization: this.authorization, - } - : undefined, - httpsAgent: this.getHttpsAgent(url), + headers: authHeader, + httpsAgent: this.getHttpsAgent(baseUrl), }) ); } diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index 10b20e819abb0..dd3af249aa3e7 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -369,7 +369,7 @@ export class SyntheticsService { this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); - service.syncErrors = await this.apiClient.put({ + service.syncErrors = await this.apiClient.syncMonitors({ monitors, output, licenseLevel: license.type, From 9d30965a1dc4c40eed940d1339d549ddc72c8217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 18 Apr 2023 14:10:57 +0200 Subject: [PATCH 014/100] Allow using the `--serverless` CLI flag in serverless-capable distros (#155009) --- src/cli/serve/serve.js | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 4a875d6955428..9facf94408235 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -78,25 +78,43 @@ const configPathCollector = pathCollector(); const pluginPathCollector = pathCollector(); /** - * @param {string} name - * @param {string[]} configs - * @param {'push' | 'unshift'} method + * @param {string} name The config file name + * @returns {boolean} Whether the file exists */ -function maybeAddConfig(name, configs, method) { +function configFileExists(name) { const path = resolve(getConfigDirectory(), name); try { - if (statSync(path).isFile()) { - configs[method](path); - } + return statSync(path).isFile(); } catch (err) { if (err.code === 'ENOENT') { - return; + return false; } throw err; } } +/** + * @returns {boolean} Whether the distribution can run in Serverless mode + */ +function isServerlessCapableDistribution() { + // For now, checking if the `serverless.yml` config file exists should be enough + // We could also check the following as well, but I don't think it's necessary: + // VALID_SERVERLESS_PROJECT_MODE.some((projectType) => configFileExists(`serverless.${projectType}.yml`)) + return configFileExists('serverless.yml'); +} + +/** + * @param {string} name + * @param {string[]} configs + * @param {'push' | 'unshift'} method + */ +function maybeAddConfig(name, configs, method) { + if (configFileExists(name)) { + configs[method](resolve(getConfigDirectory(), name)); + } +} + /** * @returns {string[]} */ @@ -233,8 +251,11 @@ export default function (program) { .option( '--run-examples', 'Adds plugin paths for all the Kibana example plugins and runs with no base path' - ) - .option('--serverless ', 'Start Kibana in a serverless project mode'); + ); + } + + if (isServerlessCapableDistribution()) { + command.option('--serverless ', 'Start Kibana in a serverless project mode'); } if (DEV_MODE_SUPPORTED) { From 56459aed5854e87f6336b4f2d22d9bede0cdef60 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Tue, 18 Apr 2023 14:14:53 +0200 Subject: [PATCH 015/100] [Infrastructure UI] Dinamically set sticky bar position for Hosts View (#155139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes #155114 Instead of having a fixed value for the sticky bar position, we now set it accordingly to the top position of the main app container. https://user-images.githubusercontent.com/34506779/232760797-c91f8ade-b1bc-4921-9ef7-30d4647219b5.mov Co-authored-by: Marco Antonio Ghiani --- x-pack/plugins/infra/public/apps/metrics_app.tsx | 4 +++- .../hosts/components/unified_search_bar.tsx | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/infra/public/apps/metrics_app.tsx b/x-pack/plugins/infra/public/apps/metrics_app.tsx index 799cf83b4ee44..16f19523b4591 100644 --- a/x-pack/plugins/infra/public/apps/metrics_app.tsx +++ b/x-pack/plugins/infra/public/apps/metrics_app.tsx @@ -22,6 +22,8 @@ import { CommonInfraProviders, CoreProviders } from './common_providers'; import { prepareMountElement } from './common_styles'; import { SourceProvider } from '../containers/metrics_source'; +export const METRICS_APP_DATA_TEST_SUBJ = 'infraMetricsPage'; + export const renderApp = ( core: CoreStart, plugins: InfraClientStartDeps, @@ -30,7 +32,7 @@ export const renderApp = ( ) => { const storage = new Storage(window.localStorage); - prepareMountElement(element, 'infraMetricsPage'); + prepareMountElement(element, METRICS_APP_DATA_TEST_SUBJ); ReactDOM.render( { const StickyContainer = (props: { children: React.ReactNode }) => { const { euiTheme } = useEuiTheme(); + const top = useMemo(() => { + const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); + if (!wrapper) { + return `calc(${euiTheme.size.xxxl} * 2)`; + } + + return `${wrapper.getBoundingClientRect().top}px`; + }, [euiTheme]); + return ( Date: Tue, 18 Apr 2023 14:15:34 +0200 Subject: [PATCH 016/100] Update email.asciidoc: Remove reference to Elastic Cloud email allowlist. (#153854) Removes the warning about the email allowlist in Elastic Cloud from the docs. It is not necessary anymore to allowlist individual email addresses in Elastic Cloud. The connector can now be used immediately without any additional config. Instead I added a link to the list of limitations for the Elastic Cloud connector (rate-limit etc.) --- docs/management/connectors/action-types/email.asciidoc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 6b2684c1a15b4..65bc85a7d7133 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -234,14 +234,12 @@ is considered `false`. Typically, `port: 465` uses `secure: true`, and [float] [[elasticcloud]] ==== Sending email from Elastic Cloud - -IMPORTANT: To receive notifications, the email addresses must be added to an - link:{cloud}/ec-watcher.html#ec-watcher-allowlist[allowlist] in the - Elasticsearch Service Console. Use the preconfigured email connector (`Elastic-Cloud-SMTP`) to send emails from Elastic Cloud. +NOTE: For more information on the preconfigured email connector, see link:{cloud}/ec-watcher.html#ec-cloud-email-service-limits[Elastic Cloud email service limits]. + [float] [[gmail]] ==== Sending email from Gmail From 9f126a95b09211ac439abdaf973e356657b32957 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:20:52 +0200 Subject: [PATCH 017/100] [Fleet] Add support for dynamic_namespace and dynamic_dataset (#154732) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/types/models/epm.ts | 2 + .../common/types/models/package_policy.ts | 6 + ...kage_policies_to_agent_permissions.test.ts | 54 +++++++- .../package_policies_to_agent_permissions.ts | 26 ++-- .../server/services/epm/archive/parse.test.ts | 51 +++++++ .../server/services/epm/archive/parse.ts | 10 +- .../server/services/package_policy.test.ts | 36 +++++ .../fleet/server/services/package_policy.ts | 24 +++- .../server/types/models/package_policy.ts | 2 + .../agent_policy_datastream_permissions.ts | 127 ++++++++++++++++++ .../apis/agent_policy/index.js | 3 +- .../test_logs/agent/stream/log.yml.hbs | 4 + .../data_stream/test_logs/fields/ecs.yml | 6 + .../data_stream/test_logs/fields/fields.yml | 16 +++ .../0.2.0/data_stream/test_logs/manifest.yml | 20 +++ .../test_metrics/agent/stream/cpu.yml.hbs | 6 + .../data_stream/test_metrics/fields/ecs.yml | 6 + .../test_metrics/fields/fields.yml | 16 +++ .../data_stream/test_metrics/manifest.yml | 31 +++++ .../dynamic_datastream/0.2.0/docs/README.md | 3 + .../dynamic_datastream/0.2.0/manifest.yml | 21 +++ 21 files changed, 451 insertions(+), 19 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_datastream_permissions.ts create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/agent/stream/log.yml.hbs create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/ecs.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/fields.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/manifest.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/agent/stream/cpu.yml.hbs create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/fields.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/manifest.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/docs/README.md create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/manifest.yml diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 41974a5fa04d5..86dc732e9648e 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -363,6 +363,8 @@ export interface RegistryElasticsearch { 'ingest_pipeline.name'?: string; source_mode?: 'default' | 'synthetic'; index_mode?: 'time_series'; + dynamic_dataset?: boolean; + dynamic_namespace?: boolean; } export interface RegistryDataStreamProperties { diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index a0609a5c5be52..55e75a50c3c42 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -30,9 +30,15 @@ export interface NewPackagePolicyInputStream { dataset: string; type: string; elasticsearch?: { + // TODO: these don't really need to be defined in the package policy schema and could be pulled directly from + // the package where needed. + dynamic_dataset?: boolean; + dynamic_namespace?: boolean; privileges?: { indices?: string[]; }; + + // Package policy specific values index_mode?: string; source_mode?: string; }; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 774fe790f9aa5..1f5ea87c6d6f9 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -9,6 +9,7 @@ jest.mock('../epm/packages'); import type { PackagePolicy, RegistryDataStream } from '../../types'; +import type { DataStreamMeta } from './package_policies_to_agent_permissions'; import { getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, @@ -404,7 +405,7 @@ describe('getDataStreamPrivileges()', () => { type: 'logs', dataset: 'test', hidden: true, - } as RegistryDataStream; + } as DataStreamMeta; const privileges = getDataStreamPrivileges(dataStream, 'namespace'); expect(privileges).toMatchObject({ @@ -420,7 +421,7 @@ describe('getDataStreamPrivileges()', () => { elasticsearch: { privileges: { indices: ['read', 'monitor'] }, }, - } as RegistryDataStream; + } as DataStreamMeta; const privileges = getDataStreamPrivileges(dataStream, 'namespace'); expect(privileges).toMatchObject({ @@ -428,4 +429,53 @@ describe('getDataStreamPrivileges()', () => { privileges: ['read', 'monitor'], }); }); + + it('sets a wildcard namespace when dynamic_namespace: true', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + elasticsearch: { + dynamic_namespace: true, + }, + } as DataStreamMeta; + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); + + expect(privileges).toMatchObject({ + names: ['logs-test-*'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('sets a wildcard dataset when dynamic_dataset: true', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + elasticsearch: { + dynamic_dataset: true, + }, + } as DataStreamMeta; + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); + + expect(privileges).toMatchObject({ + names: ['logs-*-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('sets a wildcard namespace and dataset when dynamic_namespace: true and dynamic_dataset: true', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + elasticsearch: { + dynamic_dataset: true, + dynamic_namespace: true, + }, + } as DataStreamMeta; + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); + + expect(privileges).toMatchObject({ + names: ['logs-*-*'], + privileges: ['auto_configure', 'create_doc'], + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index 974ec1221dc7c..02c44024421ce 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -135,29 +135,37 @@ export async function storedPackagePoliciesToAgentPermissions( return Object.fromEntries(await Promise.all(permissionEntries)); } -interface DataStreamMeta { +export interface DataStreamMeta { type: string; dataset: string; dataset_is_prefix?: boolean; hidden?: boolean; elasticsearch?: { privileges?: RegistryDataStreamPrivileges; + dynamic_namespace?: boolean; + dynamic_dataset?: boolean; }; } export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: string = '*') { - let index = `${dataStream.type}-${dataStream.dataset}`; - - if (dataStream.dataset_is_prefix) { - index = `${index}.*`; + let index = dataStream.hidden ? `.${dataStream.type}-` : `${dataStream.type}-`; + + // Determine dataset + if (dataStream.elasticsearch?.dynamic_dataset) { + index += `*`; + } else if (dataStream.dataset_is_prefix) { + index += `${dataStream.dataset}.*`; + } else { + index += dataStream.dataset; } - if (dataStream.hidden) { - index = `.${index}`; + // Determine namespace + if (dataStream.elasticsearch?.dynamic_namespace) { + index += `-*`; + } else { + index += `-${namespace}`; } - index += `-${namespace}`; - const privileges = dataStream?.elasticsearch?.privileges?.indices?.length ? dataStream.elasticsearch.privileges.indices : PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts index 444c095ace4b3..b7c2628eab19e 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts @@ -193,6 +193,24 @@ describe('parseDataStreamElasticsearchEntry', () => { }, }); }); + it('Should handle dynamic_dataset', () => { + expect( + parseDataStreamElasticsearchEntry({ + dynamic_dataset: true, + }) + ).toEqual({ + dynamic_dataset: true, + }); + }); + it('Should handle dynamic_namespace', () => { + expect( + parseDataStreamElasticsearchEntry({ + dynamic_namespace: true, + }) + ).toEqual({ + dynamic_namespace: true, + }); + }); }); describe('parseTopLevelElasticsearchEntry', () => { @@ -499,6 +517,39 @@ describe('parseAndVerifyDataStreams', () => { }, ]); }); + + it('should parse dotted elasticsearch keys', async () => { + expect( + parseAndVerifyDataStreams({ + paths: ['input-only-0.1.0/data_stream/stream1/manifest.yml'], + pkgName: 'input-only', + pkgVersion: '0.1.0', + manifests: { + 'input-only-0.1.0/data_stream/stream1/manifest.yml': Buffer.from( + ` + title: Custom Logs + type: logs + dataset: ds + version: 0.1.0 + elasticsearch.dynamic_dataset: true`, + 'utf8' + ), + }, + }) + ).toEqual([ + { + dataset: 'ds', + elasticsearch: { + dynamic_dataset: true, + }, + package: 'input-only', + path: 'stream1', + release: 'ga', + title: 'Custom Logs', + type: 'logs', + }, + ]); + }); }); describe('parseAndVerifyStreams', () => { diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts index 8ec5312aa1484..a55276dc621e3 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts @@ -324,7 +324,7 @@ export function parseAndVerifyDataStreams(opts: { streams: manifestStreams, elasticsearch, ...restOfProps - } = manifest; + } = expandDottedObject(manifest); if (!(dataStreamTitle && type)) { throw new PackageInvalidArchiveError( @@ -578,6 +578,14 @@ export function parseDataStreamElasticsearchEntry( parsedElasticsearchEntry.index_mode = expandedElasticsearch.index_mode; } + if (expandedElasticsearch?.dynamic_dataset) { + parsedElasticsearchEntry.dynamic_dataset = expandedElasticsearch.dynamic_dataset; + } + + if (expandedElasticsearch?.dynamic_namespace) { + parsedElasticsearchEntry.dynamic_namespace = expandedElasticsearch.dynamic_namespace; + } + return parsedElasticsearchEntry; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 314dc6c5f1bd1..ab1555270e710 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -4956,6 +4956,42 @@ describe('_applyIndexPrivileges()', () => { expect(streamOut).toEqual(inputStream); }); + it('should apply dynamic_dataset', () => { + const packageStream = createPackageStream(); + packageStream.elasticsearch = { dynamic_dataset: true }; + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + dynamic_dataset: true, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); + + it('should apply dynamic_namespace', () => { + const packageStream = createPackageStream(); + packageStream.elasticsearch = { dynamic_namespace: true }; + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + dynamic_namespace: true, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); + it('should not apply privileges if all privileges are forbidden', () => { const forbiddenPrivileges = ['write', 'delete', 'delete_index', 'all']; const packageStream = createPackageStream(forbiddenPrivileges); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 759a6bc3d05c3..c746d62cb49fd 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -6,7 +6,7 @@ */ /* eslint-disable max-classes-per-file */ -import { omit, partition, isEqual } from 'lodash'; +import { omit, partition, isEqual, cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import semverLt from 'semver/functions/lt'; import { getFlattenedObject } from '@kbn/std'; @@ -1738,11 +1738,24 @@ async function _compilePackageStreams( } // temporary export to enable testing pending refactor https://github.com/elastic/kibana/issues/112386 +// TODO: Move this logic into `package_policies_to_agent_permissions.ts` since this is not a package policy concern +// and is based entirely on the package contents export function _applyIndexPrivileges( packageDataStream: RegistryDataStream, stream: PackagePolicyInputStream ): PackagePolicyInputStream { - const streamOut = { ...stream }; + const streamOut = cloneDeep(stream); + + if (packageDataStream?.elasticsearch?.dynamic_dataset) { + streamOut.data_stream.elasticsearch = streamOut.data_stream.elasticsearch ?? {}; + streamOut.data_stream.elasticsearch.dynamic_dataset = + packageDataStream.elasticsearch.dynamic_dataset; + } + if (packageDataStream?.elasticsearch?.dynamic_namespace) { + streamOut.data_stream.elasticsearch = streamOut.data_stream.elasticsearch ?? {}; + streamOut.data_stream.elasticsearch.dynamic_namespace = + packageDataStream.elasticsearch.dynamic_namespace; + } const indexPrivileges = packageDataStream?.elasticsearch?.privileges?.indices; @@ -1763,10 +1776,9 @@ export function _applyIndexPrivileges( } if (valid.length) { - stream.data_stream.elasticsearch = { - privileges: { - indices: valid, - }, + streamOut.data_stream.elasticsearch = streamOut.data_stream.elasticsearch ?? {}; + streamOut.data_stream.elasticsearch.privileges = { + indices: valid, }; } diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 32199c2e8be48..b94becf1fe449 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -45,6 +45,8 @@ const PackagePolicyStreamsSchema = { indices: schema.maybe(schema.arrayOf(schema.string())), }) ), + dynamic_dataset: schema.maybe(schema.boolean()), + dynamic_namespace: schema.maybe(schema.boolean()), }) ), }), diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_datastream_permissions.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_datastream_permissions.ts new file mode 100644 index 0000000000000..9fe3f24b14a73 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_datastream_permissions.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { setupFleetAndAgents } from '../agents/services'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('datastream privileges', () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + }); + setupFleetAndAgents(providerContext); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + }); + + describe('dynamic privileges', () => { + // Use the dynamic_datastreams test package + before(async () => { + await supertest + .post(`/api/fleet/epm/packages/dynamic_datastream/1.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + after(async () => { + await supertest + .delete(`/api/fleet/epm/packages/dynamic_datastream/1.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + + it('correctly specifies wildcards for dynamic_dataset and dynamic_namespace', async () => { + // Create agent policy + const { + body: { + item: { id: agentPolicyId }, + }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Test policy ${uuidv4()}`, + namespace: 'default', + monitoring_enabled: [], + }) + .expect(200); + + // Create package policy + const { + body: { + item: { id: packagePolicyId }, + }, + } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `dynamic-${uuidv4()}`, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + package: { + name: 'dynamic_datastream', + version: '1.2.0', + }, + inputs: { + 'dynamic_datastream-logfile': { + enabled: true, + streams: { + 'dynamic_datastream.test_logs': { + enabled: true, + vars: { + paths: ['/var/log/auth.log*', '/var/log/secure*'], + }, + }, + }, + }, + 'dynamic_datastream-system/metrics': { + enabled: true, + streams: { + 'dynamic_datastream.test_metrics': { + enabled: true, + vars: {}, + }, + }, + }, + }, + }) + .expect(200); + + // Fetch the agent policy + const { + body: { item: fullAgentPolicy }, + } = await supertest + .get(`/api/fleet/agent_policies/${agentPolicyId}/full`) + .set('kbn-xsrf', 'xxxx'); + + // Check that the privileges are correct + expect(fullAgentPolicy.output_permissions.default[packagePolicyId].indices).to.eql([ + { names: ['logs-*-*'], privileges: ['auto_configure', 'create_doc'] }, + { names: ['metrics-*-*'], privileges: ['auto_configure', 'create_doc'] }, + ]); + + // Cleanup agent and package policy + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ agentPolicyId }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js index 8054a45acb728..21aa25cd3c0d3 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js @@ -6,8 +6,9 @@ */ export default function loadTests({ loadTestFile }) { - describe('Fleet Endpoints', () => { + describe('Agent policies', () => { loadTestFile(require.resolve('./agent_policy_with_agents_setup')); loadTestFile(require.resolve('./agent_policy')); + loadTestFile(require.resolve('./agent_policy_datastream_permissions')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/agent/stream/log.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/agent/stream/log.yml.hbs new file mode 100644 index 0000000000000..cc801fea22a77 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/agent/stream/log.yml.hbs @@ -0,0 +1,4 @@ +paths: +{{#each paths as |path i|}} + - {{path}} +{{/each}} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/ecs.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/ecs.yml new file mode 100644 index 0000000000000..7df52cc11fd20 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/ecs.yml @@ -0,0 +1,6 @@ +- name: logs_test_name + title: logs_test_title + type: text +- name: new_field_name + title: new_field_title + type: keyword diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/manifest.yml new file mode 100644 index 0000000000000..fdc69553a74b3 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_logs/manifest.yml @@ -0,0 +1,20 @@ +title: Test Dataset +type: logs +elasticsearch: + dynamic_dataset: true + dynamic_namespace: true + +streams: +- input: logfile + title: Test logs + template_path: log.yml.hbs + vars: + - name: paths + type: text + title: Paths + multi: true + required: true + show_user: true + default: + - /var/log/auth.log* + - /var/log/secure* diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/agent/stream/cpu.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/agent/stream/cpu.yml.hbs new file mode 100644 index 0000000000000..0c1ea9cdcc492 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/agent/stream/cpu.yml.hbs @@ -0,0 +1,6 @@ +metricsets: ["cpu"] +cpu.metrics: +{{#each cpu.metrics}} +- {{this}} +{{/each}} +period: {{period}} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/ecs.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/ecs.yml new file mode 100644 index 0000000000000..8fb3ccd3de8fd --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/ecs.yml @@ -0,0 +1,6 @@ +- name: metrics_test_name + title: metrics_test_title + type: keyword +- name: metrics_test_name2 + title: metrics_test_title2 + type: keyword \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/manifest.yml new file mode 100644 index 0000000000000..fa098ea9ab29f --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/data_stream/test_metrics/manifest.yml @@ -0,0 +1,31 @@ +title: Test metrics +type: metrics + +elasticsearch: + dynamic_dataset: true + dynamic_namespace: true + +streams: +- input: system/metrics + title: Test metrics + template_path: cpu.yml.hbs + vars: + - name: period + type: text + title: Period + multi: false + required: true + show_user: true + default: 10s + - name: cpu.metrics + type: text + title: Cpu Metrics + multi: true + required: true + show_user: true + description: > + How to report CPU metrics. Can be "percentages", "normalized_percentages", or "ticks" + + default: + - percentages + - normalized_percentages diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/docs/README.md new file mode 100644 index 0000000000000..08b9bf19c4e17 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing that dynamic_dataset and dynamic_namespace generate the correct API key privileges diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/manifest.yml new file mode 100644 index 0000000000000..2cd4eaa4e962b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/dynamic_datastream/0.2.0/manifest.yml @@ -0,0 +1,21 @@ +format_version: 1.0.0 +name: dynamic_datastream +title: dynamic_datastream test +description: This is a test package for testing that dynamic_dataset and dynamic_namespace generate the correct API key privileges +version: 1.2.0 +categories: [] +release: ga +type: integration +license: basic +owner: + github: elastic/fleet + +policy_templates: + - name: dynamic_datastream + title: Dynamic datastream + description: foo + inputs: + - type: logfile + title: Test dynamic logs + - type: system/metrics + title: Test dynamic metrics From 581bfb37b3d922a93b0a1fd4e54a86a279ea04b7 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 18 Apr 2023 15:23:43 +0300 Subject: [PATCH 018/100] [Lens] Improve sampling notification title (#155127) ## Summary Improves the title of the notification popover for sampled data per feedback given image --- x-pack/plugins/lens/public/datasources/form_based/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx index d14cf42e3c31b..c136aab767b65 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx @@ -514,7 +514,7 @@ export function getNotifiableFeatures( severity: 'info', fixableInEditor: false, shortMessage: i18n.translate('xpack.lens.indexPattern.samplingPerLayer', { - defaultMessage: 'Layers with reduced sampling', + defaultMessage: 'Sampling probability by layer', }), longMessage: ( Date: Tue, 18 Apr 2023 08:34:07 -0400 Subject: [PATCH 019/100] [Security Solution][Endpoint] Update test data for endpoint list FTR tests and un-skip test suite (#155060) ## Summary - Update test data for endpoint list FTR tests and un-skip test suite Fixes #154916 Fixes #154917 --- .../apps/endpoint/endpoint_list.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 0bfb0cc5bba1d..5d19932090168 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -37,7 +37,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'x', 'x', 'Warning', - 'Linux', + 'macOS', '10.2.17.24, 10.56.215.200,10.254.196.130', 'x', 'x', @@ -59,7 +59,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'x', 'x', 'Warning', - 'Linux', + 'macOS', '10.87.11.145, 10.117.106.109,10.242.136.97', 'x', 'x', @@ -81,9 +81,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return tableData; }; - // Failing: See https://github.com/elastic/kibana/issues/154917 - // Failing: See https://github.com/elastic/kibana/issues/154916 - describe.skip('endpoint list', function () { + describe('endpoint list', function () { const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); let indexedData: IndexedHostsAndAlertsResponse; describe('when initially navigating to page', () => { From 7235345601ec86df804afe1d8853aa8627482569 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 18 Apr 2023 07:36:20 -0500 Subject: [PATCH 020/100] [data view field editor] Runtime field code editor - move state out of controller (#155107) ## Summary Resolves odd behavior with the runtime field code editor - most common case is inability to remove last character. Move some field state back to react and out of controller. Fixes https://github.com/elastic/kibana/issues/154351 --- .../field_editor/form_fields/script_field.tsx | 7 +++++-- .../components/preview/field_preview_context.tsx | 15 +++++++++------ .../components/preview/preview_controller.ts | 2 ++ .../public/components/preview/types.ts | 5 +++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx index 6a53ae14186de..fddf14c864743 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -59,6 +59,9 @@ const currentDocumentSelector = (state: PreviewState) => state.documents[state.c const currentDocumentIsLoadingSelector = (state: PreviewState) => state.isLoadingDocuments; const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => { + const { + validation: { setScriptEditorValidation }, + } = useFieldPreviewContext(); const monacoEditor = useRef(null); const editorValidationSubscription = useRef(); const fieldCurrentValue = useRef(''); @@ -143,7 +146,7 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr editorValidationSubscription.current = PainlessLang.validation$().subscribe( ({ isValid, isValidating, errors }) => { - controller.setScriptEditorValidation({ + setScriptEditorValidation({ isValid, isValidating, message: errors[0]?.message ?? null, @@ -151,7 +154,7 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr } ); }, - [controller] + [setScriptEditorValidation] ); const updateMonacoMarkers = useCallback((markers: monaco.editor.IMarkerData[]) => { diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx index 3addd448f1e7e..f554025ce9f4b 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx @@ -75,8 +75,6 @@ const documentsSelector = (state: PreviewState) => { }; }; -const scriptEditorValidationSelector = (state: PreviewState) => state.scriptEditorValidation; - export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewController }> = ({ controller, children, @@ -121,6 +119,12 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro /** The parameters required for the Painless _execute API */ const [params, setParams] = useState(defaultParams); + const [scriptEditorValidation, setScriptEditorValidation] = useState<{ + isValidating: boolean; + isValid: boolean; + message: string | null; + }>({ isValidating: false, isValid: true, message: null }); + /** Flag to show/hide the preview panel */ const [isPanelVisible, setIsPanelVisible] = useState(true); /** Flag to indicate if we are loading document from cluster */ @@ -133,10 +137,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const { currentDocument, currentDocIndex, currentDocId, totalDocs, currentIdx } = useStateSelector(controller.state$, documentsSelector); - const scriptEditorValidation = useStateSelector( - controller.state$, - scriptEditorValidationSelector - ); let isPreviewAvailable = true; @@ -513,6 +513,9 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro isVisible: isPanelVisible, setIsVisible: setIsPanelVisible, }, + validation: { + setScriptEditorValidation, + }, reset, }), [ diff --git a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts index 80b11e74597aa..b572827eac06d 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts @@ -95,9 +95,11 @@ export class PreviewController { } }; + /* disabled while investigating issues with painless script editor setScriptEditorValidation = (scriptEditorValidation: PreviewState['scriptEditorValidation']) => { this.updateState({ scriptEditorValidation }); }; + */ setCustomId = (customId?: string) => { this.updateState({ customId }); diff --git a/src/plugins/data_view_field_editor/public/components/preview/types.ts b/src/plugins/data_view_field_editor/public/components/preview/types.ts index 377aed627ba54..347e0a709cf28 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/types.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/types.ts @@ -133,6 +133,11 @@ export interface Context { isLastDoc: boolean; }; reset: () => void; + validation: { + setScriptEditorValidation: React.Dispatch< + React.SetStateAction<{ isValid: boolean; isValidating: boolean; message: string | null }> + >; + }; } export type PainlessExecuteContext = From dec97d412980838ea9d897062f2987bdb6cd0159 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 18 Apr 2023 06:36:58 -0600 Subject: [PATCH 021/100] [maps] reduce bundle size (#154789) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 6 + x-pack/plugins/maps/common/i18n_getters.ts | 12 - .../public/api/create_layer_descriptors.ts | 19 +- x-pack/plugins/maps/public/api/ems.ts | 7 +- .../maps/public/classes/layers/index.ts | 2 +- .../assign_feature_ids.test.ts | 3 +- .../assign_feature_ids.ts | 5 +- .../geojson_vector_layer.tsx | 2 +- .../public/classes/layers/wizards/index.ts | 2 +- .../map_container/map_container.tsx | 1 + .../maps/public/embeddable/map_component.tsx | 85 ++---- .../maps/public/embeddable/map_embeddable.tsx | 21 +- .../embeddable/map_embeddable_factory.ts | 11 +- .../maps/public/feature_catalogue_entry.ts | 5 +- x-pack/plugins/maps/public/index.ts | 6 +- x-pack/plugins/maps/public/kibana_services.ts | 29 +- .../maps/public/lazy_load_bundle/index.ts | 106 -------- .../public/lazy_load_bundle/lazy/index.ts | 17 -- .../public/legacy_visualizations/index.ts | 1 - .../region_map/region_map_visualization.tsx | 8 +- .../tile_map/tile_map_visualization.tsx | 10 +- x-pack/plugins/maps/public/locators.ts | 257 ------------------ .../locators/map_locator/get_location.ts | 50 ++++ .../map_locator/locator_definition.test.ts} | 8 +- .../map_locator/locator_definition.ts | 22 ++ .../maps/public/locators/map_locator/types.ts | 64 +++++ .../region_map_locator/get_location.ts | 55 ++++ .../region_map_locator/locator_definition.ts | 24 ++ .../locators/region_map_locator/types.ts | 33 +++ .../locators/tile_map_locator/get_location.ts | 52 ++++ .../tile_map_locator/locator_definition.ts | 24 ++ .../public/locators/tile_map_locator/types.ts | 31 +++ .../maps/public/map_attribute_service.ts | 5 +- x-pack/plugins/maps/public/plugin.ts | 27 +- .../routes/list_page/maps_list_view.tsx | 9 +- .../routes/map_page/map_app/map_app.tsx | 12 +- .../map_page/saved_map/get_breadcrumbs.tsx | 4 +- .../public/routes/map_page/top_nav_config.tsx | 4 +- .../action.ts} | 34 +-- .../filter_by_map_extent/is_compatible.ts | 17 ++ .../modal.tsx} | 13 +- .../filter_by_map_extent/types.ts | 16 ++ .../public/trigger_actions/get_maps_link.ts | 50 ++++ .../synchronize_movement/action.ts | 40 +++ .../synchronize_movement/is_compatible.ts | 32 +++ .../modal.tsx} | 13 +- .../synchronize_movement/types.ts | 12 + .../synchronize_movement_action.tsx | 69 ----- .../visualize_geo_field_action.ts | 50 +--- x-pack/plugins/maps/public/util.ts | 5 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 53 files changed, 679 insertions(+), 714 deletions(-) delete mode 100644 x-pack/plugins/maps/public/lazy_load_bundle/index.ts delete mode 100644 x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts delete mode 100644 x-pack/plugins/maps/public/locators.ts create mode 100644 x-pack/plugins/maps/public/locators/map_locator/get_location.ts rename x-pack/plugins/maps/public/{locators.test.ts => locators/map_locator/locator_definition.test.ts} (92%) create mode 100644 x-pack/plugins/maps/public/locators/map_locator/locator_definition.ts create mode 100644 x-pack/plugins/maps/public/locators/map_locator/types.ts create mode 100644 x-pack/plugins/maps/public/locators/region_map_locator/get_location.ts create mode 100644 x-pack/plugins/maps/public/locators/region_map_locator/locator_definition.ts create mode 100644 x-pack/plugins/maps/public/locators/region_map_locator/types.ts create mode 100644 x-pack/plugins/maps/public/locators/tile_map_locator/get_location.ts create mode 100644 x-pack/plugins/maps/public/locators/tile_map_locator/locator_definition.ts create mode 100644 x-pack/plugins/maps/public/locators/tile_map_locator/types.ts rename x-pack/plugins/maps/public/trigger_actions/{filter_by_map_extent_action.tsx => filter_by_map_extent/action.ts} (63%) create mode 100644 x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/is_compatible.ts rename x-pack/plugins/maps/public/trigger_actions/{filter_by_map_extent_modal.tsx => filter_by_map_extent/modal.tsx} (78%) create mode 100644 x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/types.ts create mode 100644 x-pack/plugins/maps/public/trigger_actions/get_maps_link.ts create mode 100644 x-pack/plugins/maps/public/trigger_actions/synchronize_movement/action.ts create mode 100644 x-pack/plugins/maps/public/trigger_actions/synchronize_movement/is_compatible.ts rename x-pack/plugins/maps/public/trigger_actions/{synchronize_movement_modal.tsx => synchronize_movement/modal.tsx} (84%) create mode 100644 x-pack/plugins/maps/public/trigger_actions/synchronize_movement/types.ts delete mode 100644 x-pack/plugins/maps/public/trigger_actions/synchronize_movement_action.tsx diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index c4dab3f156caa..6c8f719bf2886 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -15,6 +15,10 @@ export const APP_ICON_SOLUTION = 'logoKibana'; export const APP_NAME = i18n.translate('xpack.maps.visTypeAlias.title', { defaultMessage: 'Maps', }); +export const MAP_EMBEDDABLE_NAME = i18n.translate('xpack.maps.embeddableDisplayName', { + defaultMessage: 'map', +}); + export const INITIAL_LAYERS_KEY = 'initialLayers'; export const MAPS_APP_PATH = `app/${APP_ID}`; @@ -36,6 +40,8 @@ export const OPEN_LAYER_WIZARD = 'openLayerWizard'; // Centroids are a single point for representing lines, multiLines, polygons, and multiPolygons export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; +export const GEOJSON_FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; + export function getNewMapPath() { return `/${MAPS_APP_PATH}/${MAP_PATH}`; } diff --git a/x-pack/plugins/maps/common/i18n_getters.ts b/x-pack/plugins/maps/common/i18n_getters.ts index 0c59cc891504e..09df14ef9f289 100644 --- a/x-pack/plugins/maps/common/i18n_getters.ts +++ b/x-pack/plugins/maps/common/i18n_getters.ts @@ -9,18 +9,6 @@ import { i18n } from '@kbn/i18n'; import { ES_SPATIAL_RELATIONS } from './constants'; -export function getAppTitle() { - return i18n.translate('xpack.maps.appTitle', { - defaultMessage: 'Maps', - }); -} - -export function getMapEmbeddableDisplayName() { - return i18n.translate('xpack.maps.embeddableDisplayName', { - defaultMessage: 'map', - }); -} - export function getDataSourceLabel() { return i18n.translate('xpack.maps.source.dataSourceLabel', { defaultMessage: 'Data source', diff --git a/x-pack/plugins/maps/public/api/create_layer_descriptors.ts b/x-pack/plugins/maps/public/api/create_layer_descriptors.ts index cfc914ef7c0f4..553abcf7d5481 100644 --- a/x-pack/plugins/maps/public/api/create_layer_descriptors.ts +++ b/x-pack/plugins/maps/public/api/create_layer_descriptors.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { LayerDescriptor } from '../../common/descriptor_types'; -import { lazyLoadMapModules } from '../lazy_load_bundle'; +import type { LayerDescriptor } from '../../common/descriptor_types'; import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; export const createLayerDescriptors = { @@ -14,17 +13,21 @@ export const createLayerDescriptors = { indexPatternId: string, indexPatternTitle: string ): Promise { - const mapModules = await lazyLoadMapModules(); - return mapModules.createSecurityLayerDescriptors(indexPatternId, indexPatternTitle); + const { createSecurityLayerDescriptors } = await import( + '../classes/layers/wizards/solution_layers/security' + ); + return createSecurityLayerDescriptors(indexPatternId, indexPatternTitle); }, async createBasemapLayerDescriptor(): Promise { - const mapModules = await lazyLoadMapModules(); - return mapModules.createBasemapLayerDescriptor(); + const { createBasemapLayerDescriptor } = await import( + '../classes/layers/create_basemap_layer_descriptor' + ); + return createBasemapLayerDescriptor(); }, async createESSearchSourceLayerDescriptor( params: CreateLayerDescriptorParams ): Promise { - const mapModules = await lazyLoadMapModules(); - return mapModules.createESSearchSourceLayerDescriptor(params); + const { createLayerDescriptor } = await import('../classes/sources/es_search_source'); + return createLayerDescriptor(params); }, }; diff --git a/x-pack/plugins/maps/public/api/ems.ts b/x-pack/plugins/maps/public/api/ems.ts index da6e88c84e22c..64395bddefab4 100644 --- a/x-pack/plugins/maps/public/api/ems.ts +++ b/x-pack/plugins/maps/public/api/ems.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest'; -import { lazyLoadMapModules } from '../lazy_load_bundle'; +import type { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest'; export async function suggestEMSTermJoinConfig( sampleValuesConfig: SampleValuesConfig ): Promise { - const mapModules = await lazyLoadMapModules(); - return await mapModules.suggestEMSTermJoinConfig(sampleValuesConfig); + const { suggestEMSTermJoinConfig: suggestEms } = await import('../ems_autosuggest'); + return await suggestEms(sampleValuesConfig); } diff --git a/x-pack/plugins/maps/public/classes/layers/index.ts b/x-pack/plugins/maps/public/classes/layers/index.ts index b068d4d234170..fe7e297ba8a1d 100644 --- a/x-pack/plugins/maps/public/classes/layers/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/index.ts @@ -6,4 +6,4 @@ */ export type { LayerWizard, LayerWizardWithMeta, RenderWizardArguments } from './wizards'; -export { getLayerWizards, registerLayerWizardExternal } from './wizards'; +export { getLayerWizards } from './wizards'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts index 2250e86da0ec2..d4a34c9104a2e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { assignFeatureIds, GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; +import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from '../../../../../common/constants'; +import { assignFeatureIds } from './assign_feature_ids'; import { FeatureCollection, Feature, Point } from 'geojson'; const featureId = 'myFeature1'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts index 3611256d246fb..f4b39c7a0d324 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts @@ -6,9 +6,8 @@ */ import _ from 'lodash'; -import { FeatureCollection, Feature } from 'geojson'; - -export const GEOJSON_FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; +import type { FeatureCollection, Feature } from 'geojson'; +import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from '../../../../../common/constants'; let idCounter = 0; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx index 419757e24633c..a337cdbfaa7a5 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -14,6 +14,7 @@ import type { FilterSpecification, Map as MbMap, GeoJSONSource } from '@kbn/mapb import { EMPTY_FEATURE_COLLECTION, FEATURE_VISIBLE_PROPERTY_NAME, + GEOJSON_FEATURE_ID_PROPERTY_NAME, LAYER_TYPE, SOURCE_BOUNDS_DATA_REQUEST_ID, } from '../../../../../common/constants'; @@ -35,7 +36,6 @@ import { } from '../vector_layer'; import { DataRequestAbortError } from '../../../util/data_request'; import { getFeatureCollectionBounds } from '../../../util/get_feature_collection_bounds'; -import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; import { syncGeojsonSourceData } from './geojson_source_data'; import { performInnerJoins } from './perform_inner_joins'; import { pluckStyleMetaFromFeatures } from './pluck_style_meta_from_features'; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts index 814a2ec8e5c2f..ecb690b3d1f15 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts @@ -10,4 +10,4 @@ export type { LayerWizardWithMeta, RenderWizardArguments, } from './layer_wizard_registry'; -export { getLayerWizards, registerLayerWizardExternal } from './layer_wizard_registry'; +export { getLayerWizards } from './layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index e11b1a0530466..fa77048f6b88e 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import '../../_index.scss'; import React, { Component } from 'react'; import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; diff --git a/x-pack/plugins/maps/public/embeddable/map_component.tsx b/x-pack/plugins/maps/public/embeddable/map_component.tsx index 13a589ebd0946..2e080a46d8478 100644 --- a/x-pack/plugins/maps/public/embeddable/map_component.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_component.tsx @@ -8,25 +8,19 @@ import React, { Component, RefObject } from 'react'; import { first } from 'rxjs/operators'; import { v4 as uuidv4 } from 'uuid'; -import { EuiLoadingChart } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import type { Query, TimeRange } from '@kbn/es-query'; import type { LayerDescriptor, MapCenterAndZoom } from '../../common/descriptor_types'; import type { MapEmbeddableType } from './types'; -import type { LazyLoadedMapModules } from '../lazy_load_bundle'; -import { lazyLoadMapModules } from '../lazy_load_bundle'; +import { MapEmbeddable } from './map_embeddable'; +import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor'; interface Props { title: string; filters?: Filter[]; query?: Query; timeRange?: TimeRange; - getLayerDescriptors: ( - mapModules: Pick< - LazyLoadedMapModules, - 'createTileMapLayerDescriptor' | 'createRegionMapLayerDescriptor' - > - ) => LayerDescriptor[]; + getLayerDescriptors: () => LayerDescriptor[]; mapCenter?: MapCenterAndZoom; onInitialRenderComplete?: () => void; /* @@ -35,48 +29,13 @@ interface Props { isSharable?: boolean; } -interface State { - isLoaded: boolean; -} - -export class MapComponent extends Component { - private _isMounted = false; - private _mapEmbeddable?: MapEmbeddableType | undefined; +export class MapComponent extends Component { + private _mapEmbeddable: MapEmbeddableType; private readonly _embeddableRef: RefObject = React.createRef(); - state: State = { isLoaded: false }; - - componentDidMount() { - this._isMounted = true; - this._load(); - } - - componentWillUnmount() { - this._isMounted = false; - if (this._mapEmbeddable) { - this._mapEmbeddable.destroy(); - } - } - - componentDidUpdate() { - if (this._mapEmbeddable) { - this._mapEmbeddable.updateInput({ - filters: this.props.filters, - query: this.props.query, - timeRange: this.props.timeRange, - }); - } - } - - async _load() { - const mapModules = await lazyLoadMapModules(); - if (!this._isMounted) { - return; - } - - this.setState({ isLoaded: true }); - - this._mapEmbeddable = new mapModules.MapEmbeddable( + constructor(props: Props) { + super(props); + this._mapEmbeddable = new MapEmbeddable( { editable: false, }, @@ -85,11 +44,8 @@ export class MapComponent extends Component { attributes: { title: this.props.title, layerListJSON: JSON.stringify([ - mapModules.createBasemapLayerDescriptor(), - ...this.props.getLayerDescriptors({ - createRegionMapLayerDescriptor: mapModules.createRegionMapLayerDescriptor, - createTileMapLayerDescriptor: mapModules.createTileMapLayerDescriptor, - }), + createBasemapLayerDescriptor(), + ...this.props.getLayerDescriptors(), ]), }, mapCenter: this.props.mapCenter, @@ -101,7 +57,7 @@ export class MapComponent extends Component { .getOnRenderComplete$() .pipe(first()) .subscribe(() => { - if (this._isMounted && this.props.onInitialRenderComplete) { + if (this.props.onInitialRenderComplete) { this.props.onInitialRenderComplete(); } }); @@ -110,16 +66,27 @@ export class MapComponent extends Component { if (this.props.isSharable !== undefined) { this._mapEmbeddable.setIsSharable(this.props.isSharable); } + } + + componentDidMount() { if (this._embeddableRef.current) { this._mapEmbeddable.render(this._embeddableRef.current); } } - render() { - if (!this.state.isLoaded) { - return ; - } + componentWillUnmount() { + this._mapEmbeddable.destroy(); + } + componentDidUpdate() { + this._mapEmbeddable.updateInput({ + filters: this.props.filters, + query: this.props.query, + timeRange: this.props.timeRange, + }); + } + + render() { return
; } } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 33552613c0c95..834023182f45b 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -21,6 +21,7 @@ import { startWith, } from 'rxjs/operators'; import { Unsubscribe } from 'redux'; +import type { PaletteRegistry } from '@kbn/coloring'; import type { KibanaExecutionContext } from '@kbn/core/public'; import { EuiEmptyPrompt } from '@elastic/eui'; import { type Filter } from '@kbn/es-query'; @@ -82,7 +83,7 @@ import { } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; import { - getChartsPaletteServiceGetColor, + getCharts, getCoreI18n, getExecutionContextService, getHttp, @@ -109,6 +110,24 @@ import { MapEmbeddableOutput, } from './types'; +async function getChartsPaletteServiceGetColor(): Promise<((value: string) => string) | null> { + const chartsService = getCharts(); + const paletteRegistry: PaletteRegistry | null = chartsService + ? await chartsService.palettes.getPalettes() + : null; + if (!paletteRegistry) { + return null; + } + + const paletteDefinition = paletteRegistry.get('default'); + const chartConfiguration = { syncColors: true }; + return (value: string) => { + const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }]; + const color = paletteDefinition.getCategoricalColor(series, chartConfiguration); + return color ? color : '#3d3d3d'; + }; +} + function getIsRestore(searchSessionId?: string) { if (!searchSessionId) { return false; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 3642448774c58..ae9d4b6d39329 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -8,12 +8,10 @@ import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public'; -import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; -import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; +import { MAP_SAVED_OBJECT_TYPE, APP_ICON, MAP_EMBEDDABLE_NAME } from '../../common/constants'; import { extract, inject } from '../../common/embeddable'; import { MapByReferenceInput, MapEmbeddableInput } from './types'; -import { lazyLoadMapModules } from '../lazy_load_bundle'; -import { getApplication, getUsageCollection } from '../kibana_services'; +import { getApplication, getMapsCapabilities, getUsageCollection } from '../kibana_services'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; @@ -26,7 +24,6 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { }; async isEditable() { - const { getMapsCapabilities } = await lazyLoadMapModules(); return getMapsCapabilities().save as boolean; } @@ -36,7 +33,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { } getDisplayName() { - return getMapEmbeddableDisplayName(); + return MAP_EMBEDDABLE_NAME; } createFromSavedObject = async ( @@ -51,7 +48,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { }; create = async (input: MapEmbeddableInput, parent?: IContainer) => { - const { MapEmbeddable } = await lazyLoadMapModules(); + const { MapEmbeddable } = await import('./map_embeddable'); const usageCollection = getUsageCollection(); if (usageCollection) { // currentAppId$ is a BehaviorSubject exposed as an observable so subscription gets last value upon subscribe diff --git a/x-pack/plugins/maps/public/feature_catalogue_entry.ts b/x-pack/plugins/maps/public/feature_catalogue_entry.ts index 80d37aa0288e7..b897795e2eb49 100644 --- a/x-pack/plugins/maps/public/feature_catalogue_entry.ts +++ b/x-pack/plugins/maps/public/feature_catalogue_entry.ts @@ -7,12 +7,11 @@ import { i18n } from '@kbn/i18n'; import type { FeatureCatalogueCategory } from '@kbn/home-plugin/public'; -import { APP_ID, APP_ICON } from '../common/constants'; -import { getAppTitle } from '../common/i18n_getters'; +import { APP_ID, APP_ICON, APP_NAME } from '../common/constants'; export const featureCatalogueEntry = { id: APP_ID, - title: getAppTitle(), + title: APP_NAME, subtitle: i18n.translate('xpack.maps.featureCatalogue.mapsSubtitle', { defaultMessage: 'Plot geographic data.', }), diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index beb0d5153d89e..e88a167a6da24 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -17,12 +17,10 @@ export const plugin: PluginInitializer = ( return new MapsPlugin(initContext); }; -export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -export { MAPS_APP_LOCATOR } from './locators'; +export { GEOJSON_FEATURE_ID_PROPERTY_NAME, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +export { MAPS_APP_LOCATOR } from './locators/map_locator/locator_definition'; export type { PreIndexedShape } from '../common/elasticsearch_util'; -export { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids'; - export type { ITooltipProperty, RenderTooltipContentParams, diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 315f75c313fa5..623f82a473472 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -6,7 +6,6 @@ */ import type { CoreStart } from '@kbn/core/public'; -import type { PaletteRegistry } from '@kbn/coloring'; import type { EMSSettings } from '@kbn/maps-ems-plugin/common/ems_settings'; import { MapsEmsPluginPublicStart } from '@kbn/maps-ems-plugin/public'; import type { MapsConfigType } from '../config'; @@ -50,6 +49,7 @@ export const getMapsCapabilities = () => coreStart.application.capabilities.maps export const getVisualizeCapabilities = () => coreStart.application.capabilities.visualize; export const getDocLinks = () => coreStart.docLinks; export const getCoreOverlays = () => coreStart.overlays; +export const getCharts = () => pluginsStart.charts; export const getData = () => pluginsStart.data; export const getUiActions = () => pluginsStart.uiActions; export const getCore = () => coreStart; @@ -90,34 +90,7 @@ export const getEMSSettings: () => EMSSettings = () => { export const getEmsTileLayerId = () => mapsEms.config.emsTileLayerId; -export const getTilemap = () => { - if (mapsEms.config.tilemap) { - return mapsEms.config.tilemap; - } else { - return {}; - } -}; - export const getShareService = () => pluginsStart.share; export const getIsAllowByValueEmbeddables = () => pluginsStart.dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; - -export async function getChartsPaletteServiceGetColor(): Promise< - ((value: string) => string) | null -> { - const paletteRegistry: PaletteRegistry | null = pluginsStart.charts - ? await pluginsStart.charts.palettes.getPalettes() - : null; - if (!paletteRegistry) { - return null; - } - - const paletteDefinition = paletteRegistry.get('default'); - const chartConfiguration = { syncColors: true }; - return (value: string) => { - const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }]; - const color = paletteDefinition.getCategoricalColor(series, chartConfiguration); - return color ? color : '#3d3d3d'; - }; -} diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts deleted file mode 100644 index a2f4f4004797e..0000000000000 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataViewsContract } from '@kbn/data-views-plugin/common'; -import { AppMountParameters, CoreStart } from '@kbn/core/public'; -import { IContainer } from '@kbn/embeddable-plugin/public'; -import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; -import { LayerDescriptor } from '../../common/descriptor_types'; -import type { - MapEmbeddableConfig, - MapEmbeddableInput, - MapEmbeddableType, -} from '../embeddable/types'; -import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; -import type { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest'; -import type { CreateTileMapLayerDescriptorParams } from '../classes/layers/create_tile_map_layer_descriptor'; -import type { CreateRegionMapLayerDescriptorParams } from '../classes/layers/create_region_map_layer_descriptor'; - -let loadModulesPromise: Promise; - -export interface LazyLoadedMapModules { - MapEmbeddable: new ( - config: MapEmbeddableConfig, - initialInput: MapEmbeddableInput, - parent?: IContainer - ) => MapEmbeddableType; - getIndexPatternService: () => DataViewsContract; - getMapsCapabilities: () => any; - renderApp: ( - params: AppMountParameters, - deps: { - coreStart: CoreStart; - AppUsageTracker: React.FC; - savedObjectsTagging?: SavedObjectTaggingPluginStart; - } - ) => Promise<() => void>; - createSecurityLayerDescriptors: ( - indexPatternId: string, - indexPatternTitle: string - ) => LayerDescriptor[]; - createTileMapLayerDescriptor: ({ - label, - mapType, - colorSchema, - indexPatternId, - geoFieldName, - metricAgg, - metricFieldName, - }: CreateTileMapLayerDescriptorParams) => LayerDescriptor | null; - createRegionMapLayerDescriptor: ({ - label, - emsLayerId, - leftFieldName, - termsFieldName, - termsSize, - colorSchema, - indexPatternId, - metricAgg, - metricFieldName, - }: CreateRegionMapLayerDescriptorParams) => LayerDescriptor | null; - createBasemapLayerDescriptor: () => LayerDescriptor | null; - createESSearchSourceLayerDescriptor: (params: CreateLayerDescriptorParams) => LayerDescriptor; - suggestEMSTermJoinConfig: (config: SampleValuesConfig) => Promise; -} - -export async function lazyLoadMapModules(): Promise { - if (typeof loadModulesPromise !== 'undefined') { - return loadModulesPromise; - } - - loadModulesPromise = new Promise(async (resolve, reject) => { - try { - const { - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - } = await import('./lazy'); - resolve({ - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - }); - } catch (error) { - reject(error); - } - }); - return loadModulesPromise; -} diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts deleted file mode 100644 index fb5321dfc03f8..0000000000000 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import '../../_index.scss'; -export * from '../../embeddable/map_embeddable'; -export * from '../../kibana_services'; -export { renderApp } from '../../render_app'; -export * from '../../classes/layers/wizards/solution_layers/security'; -export { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor'; -export { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; -export { createBasemapLayerDescriptor } from '../../classes/layers/create_basemap_layer_descriptor'; -export { createLayerDescriptor as createESSearchSourceLayerDescriptor } from '../../classes/sources/es_search_source'; -export { suggestEMSTermJoinConfig } from '../../ems_autosuggest'; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/index.ts b/x-pack/plugins/maps/public/legacy_visualizations/index.ts index d99544bbb207d..177cc608a975c 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/index.ts +++ b/x-pack/plugins/maps/public/legacy_visualizations/index.ts @@ -8,4 +8,3 @@ export { GEOHASH_GRID, getGeoHashBucketAgg } from './tile_map'; export { createRegionMapFn, regionMapRenderer, regionMapVisType } from './region_map'; export { createTileMapFn, tileMapRenderer, tileMapVisType } from './tile_map'; -export { isLegacyMap } from './is_legacy_map'; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx index b6945995da9d9..4dedb97202857 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx @@ -9,8 +9,8 @@ import React from 'react'; import type { Filter } from '@kbn/es-query'; import type { Query, TimeRange } from '@kbn/es-query'; import { RegionMapVisConfig } from './types'; -import type { LazyLoadedMapModules } from '../../lazy_load_bundle'; import { MapComponent } from '../../embeddable/map_component'; +import { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; interface Props { filters?: Filter[]; @@ -26,11 +26,7 @@ function RegionMapVisualization(props: Props) { lon: props.visConfig.mapCenter[1], zoom: props.visConfig.mapZoom, }; - function getLayerDescriptors({ - createRegionMapLayerDescriptor, - }: { - createRegionMapLayerDescriptor: LazyLoadedMapModules['createRegionMapLayerDescriptor']; - }) { + function getLayerDescriptors() { const layerDescriptor = createRegionMapLayerDescriptor(props.visConfig.layerDescriptorParams); return layerDescriptor ? [layerDescriptor] : []; } diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx index 97a3609d765a4..5eb4132528de5 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx @@ -8,9 +8,9 @@ import React from 'react'; import type { Filter } from '@kbn/es-query'; import type { Query, TimeRange } from '@kbn/es-query'; -import { TileMapVisConfig } from './types'; -import type { LazyLoadedMapModules } from '../../lazy_load_bundle'; +import type { TileMapVisConfig } from './types'; import { MapComponent } from '../../embeddable/map_component'; +import { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor'; interface Props { filters?: Filter[]; @@ -26,11 +26,7 @@ function TileMapVisualization(props: Props) { lon: props.visConfig.mapCenter[1], zoom: props.visConfig.mapZoom, }; - function getLayerDescriptors({ - createTileMapLayerDescriptor, - }: { - createTileMapLayerDescriptor: LazyLoadedMapModules['createTileMapLayerDescriptor']; - }) { + function getLayerDescriptors() { const layerDescriptor = createTileMapLayerDescriptor(props.visConfig.layerDescriptorParams); return layerDescriptor ? [layerDescriptor] : []; } diff --git a/x-pack/plugins/maps/public/locators.ts b/x-pack/plugins/maps/public/locators.ts deleted file mode 100644 index ff2ac6590708e..0000000000000 --- a/x-pack/plugins/maps/public/locators.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ - -import rison from '@kbn/rison'; -import type { DataViewSpec } from '@kbn/data-views-plugin/public'; -import type { SerializableRecord } from '@kbn/utility-types'; -import { type Filter, isFilterPinned, type TimeRange, type Query } from '@kbn/es-query'; -import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import type { LayerDescriptor } from '../common/descriptor_types'; -import { INITIAL_LAYERS_KEY, APP_ID } from '../common/constants'; -import { lazyLoadMapModules } from './lazy_load_bundle'; - -export interface MapsAppLocatorParams extends SerializableRecord { - /** - * If given, it will load the given map else will load the create a new map page. - */ - mapId?: string; - - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the initial Layers. - */ - initialLayers?: LayerDescriptor[] & SerializableRecord; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval & SerializableRecord; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the - * saved map has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - - /** - * Optionally set a query. NOTE: if given and used in conjunction with `mapId`, and the - * saved map has a query saved with it, this will _replace_ that query. - */ - query?: Query; - - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - hash?: boolean; - - /** - * Optionally pass adhoc data view spec. - */ - dataViewSpec?: DataViewSpec; -} - -export const MAPS_APP_LOCATOR = 'MAPS_APP_LOCATOR' as const; - -export type MapsAppLocator = LocatorPublic; - -export interface MapsAppLocatorDependencies { - useHash: boolean; -} - -export class MapsAppLocatorDefinition implements LocatorDefinition { - public readonly id = MAPS_APP_LOCATOR; - - constructor(protected readonly deps: MapsAppLocatorDependencies) {} - - public readonly getLocation = async (params: MapsAppLocatorParams) => { - const { mapId, filters, query, refreshInterval, timeRange, initialLayers, hash } = params; - const useHash = hash ?? this.deps.useHash; - const appState: { - query?: Query; - filters?: Filter[]; - vis?: unknown; - } = {}; - const queryState: GlobalQueryStateFromUrl = {}; - - if (query) appState.query = query; - if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); - if (timeRange) queryState.time = timeRange; - if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - let path = `/map#/${mapId || ''}`; - path = setStateToKbnUrl('_g', queryState, { useHash }, path); - path = setStateToKbnUrl('_a', appState, { useHash }, path); - - if (initialLayers && initialLayers.length) { - const risonEncodedInitialLayers = rison.encodeArray(initialLayers); - path = `${path}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; - } - - return { - app: APP_ID, - path, - state: params.dataViewSpec - ? { - dataViewSpec: params.dataViewSpec, - } - : {}, - }; - }; -} - -export interface MapsAppTileMapLocatorParams extends SerializableRecord { - label: string; - mapType: string; - colorSchema: string; - indexPatternId?: string; - geoFieldName?: string; - metricAgg: string; - metricFieldName?: string; - timeRange?: TimeRange; - filters?: Filter[]; - query?: Query; - hash?: boolean; -} - -export type MapsAppTileMapLocator = LocatorPublic; - -export const MAPS_APP_TILE_MAP_LOCATOR = 'MAPS_APP_TILE_MAP_LOCATOR' as const; - -export interface MapsAppTileMapLocatorDependencies { - locator: MapsAppLocator; -} - -export class MapsAppTileMapLocatorDefinition - implements LocatorDefinition -{ - public readonly id = MAPS_APP_TILE_MAP_LOCATOR; - - constructor(protected readonly deps: MapsAppTileMapLocatorDependencies) {} - - public readonly getLocation = async (params: MapsAppTileMapLocatorParams) => { - const { - label, - mapType, - colorSchema, - indexPatternId, - geoFieldName, - metricAgg, - metricFieldName, - filters, - query, - timeRange, - hash = true, - } = params; - const mapModules = await lazyLoadMapModules(); - const initialLayers = [] as unknown as LayerDescriptor[] & SerializableRecord; - const tileMapLayerDescriptor = mapModules.createTileMapLayerDescriptor({ - label, - mapType, - colorSchema, - indexPatternId, - geoFieldName, - metricAgg, - metricFieldName, - }); - - if (tileMapLayerDescriptor) { - initialLayers.push(tileMapLayerDescriptor); - } - - return await this.deps.locator.getLocation({ - initialLayers, - filters, - query, - timeRange, - hash, - }); - }; -} - -export interface MapsAppRegionMapLocatorParams extends SerializableRecord { - label: string; - emsLayerId?: string; - leftFieldName?: string; - termsFieldName?: string; - termsSize?: number; - colorSchema: string; - indexPatternId?: string; - metricAgg: string; - metricFieldName?: string; - timeRange?: TimeRange; - filters?: Filter[]; - query?: Query; - hash?: boolean; -} - -export type MapsAppRegionMapLocator = LocatorPublic; - -export const MAPS_APP_REGION_MAP_LOCATOR = 'MAPS_APP_REGION_MAP_LOCATOR' as const; - -export interface MapsAppRegionMapLocatorDependencies { - locator: MapsAppLocator; -} - -export class MapsAppRegionMapLocatorDefinition - implements LocatorDefinition -{ - public readonly id = MAPS_APP_REGION_MAP_LOCATOR; - - constructor(protected readonly deps: MapsAppRegionMapLocatorDependencies) {} - - public readonly getLocation = async (params: MapsAppRegionMapLocatorParams) => { - const { - label, - emsLayerId, - leftFieldName, - termsFieldName, - termsSize, - colorSchema, - indexPatternId, - metricAgg, - metricFieldName, - filters, - query, - timeRange, - hash = true, - } = params; - const mapModules = await lazyLoadMapModules(); - const initialLayers = [] as unknown as LayerDescriptor[] & SerializableRecord; - const regionMapLayerDescriptor = mapModules.createRegionMapLayerDescriptor({ - label, - emsLayerId, - leftFieldName, - termsFieldName, - termsSize, - colorSchema, - indexPatternId, - metricAgg, - metricFieldName, - }); - if (regionMapLayerDescriptor) { - initialLayers.push(regionMapLayerDescriptor); - } - - return await this.deps.locator.getLocation({ - initialLayers, - filters, - query, - timeRange, - hash, - }); - }; -} diff --git a/x-pack/plugins/maps/public/locators/map_locator/get_location.ts b/x-pack/plugins/maps/public/locators/map_locator/get_location.ts new file mode 100644 index 0000000000000..2a451183dd8e0 --- /dev/null +++ b/x-pack/plugins/maps/public/locators/map_locator/get_location.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import rison from '@kbn/rison'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; +import type { Filter, Query } from '@kbn/es-query'; +import { isFilterPinned } from '@kbn/es-query'; +import { INITIAL_LAYERS_KEY, APP_ID } from '../../../common/constants'; +import type { MapsAppLocatorDependencies, MapsAppLocatorParams } from './types'; + +export function getLocation(params: MapsAppLocatorParams, deps: MapsAppLocatorDependencies) { + const { mapId, filters, query, refreshInterval, timeRange, initialLayers, hash } = params; + const useHash = hash ?? deps.useHash; + const appState: { + query?: Query; + filters?: Filter[]; + vis?: unknown; + } = {}; + const queryState: GlobalQueryStateFromUrl = {}; + + if (query) appState.query = query; + if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let path = `/map#/${mapId || ''}`; + path = setStateToKbnUrl('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_a', appState, { useHash }, path); + + if (initialLayers && initialLayers.length) { + const risonEncodedInitialLayers = rison.encodeArray(initialLayers); + path = `${path}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; + } + + return { + app: APP_ID, + path, + state: params.dataViewSpec + ? { + dataViewSpec: params.dataViewSpec, + } + : {}, + }; +} diff --git a/x-pack/plugins/maps/public/locators.test.ts b/x-pack/plugins/maps/public/locators/map_locator/locator_definition.test.ts similarity index 92% rename from x-pack/plugins/maps/public/locators.test.ts rename to x-pack/plugins/maps/public/locators/map_locator/locator_definition.test.ts index cc954d5f73717..d281ad00c2b59 100644 --- a/x-pack/plugins/maps/public/locators.test.ts +++ b/x-pack/plugins/maps/public/locators/map_locator/locator_definition.test.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; +import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../../../common/constants'; import { FilterStateStore } from '@kbn/es-query'; -import { MapsAppLocatorDefinition } from './locators'; -import { SerializableRecord } from '@kbn/utility-types'; -import { LayerDescriptor } from '../common/descriptor_types'; +import { MapsAppLocatorDefinition } from './locator_definition'; +import type { SerializableRecord } from '@kbn/utility-types'; +import type { LayerDescriptor } from '../../../common/descriptor_types'; const MAP_ID: string = '2c9c1f60-1909-11e9-919b-ffe5949a18d2'; const LAYER_ID: string = '13823000-99b9-11ea-9eb6-d9e8adceb647'; diff --git a/x-pack/plugins/maps/public/locators/map_locator/locator_definition.ts b/x-pack/plugins/maps/public/locators/map_locator/locator_definition.ts new file mode 100644 index 0000000000000..7c2d71ebeb0dd --- /dev/null +++ b/x-pack/plugins/maps/public/locators/map_locator/locator_definition.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition } from '@kbn/share-plugin/public'; +import type { MapsAppLocatorDependencies, MapsAppLocatorParams } from './types'; + +export const MAPS_APP_LOCATOR = 'MAPS_APP_LOCATOR' as const; + +export class MapsAppLocatorDefinition implements LocatorDefinition { + public readonly id = MAPS_APP_LOCATOR; + + constructor(protected readonly deps: MapsAppLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppLocatorParams) => { + const { getLocation } = await import('./get_location'); + return getLocation(params, this.deps); + }; +} diff --git a/x-pack/plugins/maps/public/locators/map_locator/types.ts b/x-pack/plugins/maps/public/locators/map_locator/types.ts new file mode 100644 index 0000000000000..bf32465cb5cc3 --- /dev/null +++ b/x-pack/plugins/maps/public/locators/map_locator/types.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { Filter, TimeRange, Query } from '@kbn/es-query'; +import type { DataViewSpec } from '@kbn/data-views-plugin/public'; +import type { RefreshInterval } from '@kbn/data-plugin/public'; +import type { LocatorPublic } from '@kbn/share-plugin/public'; +import type { LayerDescriptor } from '../../../common/descriptor_types'; + +export interface MapsAppLocatorParams extends SerializableRecord { + /** + * If given, it will load the given map else will load the create a new map page. + */ + mapId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the initial Layers. + */ + initialLayers?: LayerDescriptor[] & SerializableRecord; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableRecord; + + /** + * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the + * saved map has filters saved with it, this will _replace_ those filters. + */ + filters?: Filter[]; + + /** + * Optionally set a query. NOTE: if given and used in conjunction with `mapId`, and the + * saved map has a query saved with it, this will _replace_ that query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + hash?: boolean; + + /** + * Optionally pass adhoc data view spec. + */ + dataViewSpec?: DataViewSpec; +} + +export type MapsAppLocator = LocatorPublic; + +export interface MapsAppLocatorDependencies { + useHash: boolean; +} diff --git a/x-pack/plugins/maps/public/locators/region_map_locator/get_location.ts b/x-pack/plugins/maps/public/locators/region_map_locator/get_location.ts new file mode 100644 index 0000000000000..b7706fe8c2e4a --- /dev/null +++ b/x-pack/plugins/maps/public/locators/region_map_locator/get_location.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { LayerDescriptor } from '../../../common/descriptor_types'; +import { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; +import type { MapsAppRegionMapLocatorParams, MapsAppRegionMapLocatorDependencies } from './types'; + +export async function getLocation( + params: MapsAppRegionMapLocatorParams, + deps: MapsAppRegionMapLocatorDependencies +) { + const { + label, + emsLayerId, + leftFieldName, + termsFieldName, + termsSize, + colorSchema, + indexPatternId, + metricAgg, + metricFieldName, + filters, + query, + timeRange, + hash = true, + } = params; + const initialLayers = [] as unknown as LayerDescriptor[] & SerializableRecord; + const regionMapLayerDescriptor = createRegionMapLayerDescriptor({ + label, + emsLayerId, + leftFieldName, + termsFieldName, + termsSize, + colorSchema, + indexPatternId, + metricAgg, + metricFieldName, + }); + if (regionMapLayerDescriptor) { + initialLayers.push(regionMapLayerDescriptor); + } + + return await deps.locator.getLocation({ + initialLayers, + filters, + query, + timeRange, + hash, + }); +} diff --git a/x-pack/plugins/maps/public/locators/region_map_locator/locator_definition.ts b/x-pack/plugins/maps/public/locators/region_map_locator/locator_definition.ts new file mode 100644 index 0000000000000..b8b7e0fba3cb8 --- /dev/null +++ b/x-pack/plugins/maps/public/locators/region_map_locator/locator_definition.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition } from '@kbn/share-plugin/public'; +import type { MapsAppRegionMapLocatorParams, MapsAppRegionMapLocatorDependencies } from './types'; + +export const MAPS_APP_REGION_MAP_LOCATOR = 'MAPS_APP_REGION_MAP_LOCATOR' as const; + +export class MapsAppRegionMapLocatorDefinition + implements LocatorDefinition +{ + public readonly id = MAPS_APP_REGION_MAP_LOCATOR; + + constructor(protected readonly deps: MapsAppRegionMapLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppRegionMapLocatorParams) => { + const { getLocation } = await import('./get_location'); + return getLocation(params, this.deps); + }; +} diff --git a/x-pack/plugins/maps/public/locators/region_map_locator/types.ts b/x-pack/plugins/maps/public/locators/region_map_locator/types.ts new file mode 100644 index 0000000000000..f42ee3554ee0c --- /dev/null +++ b/x-pack/plugins/maps/public/locators/region_map_locator/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { Filter, TimeRange, Query } from '@kbn/es-query'; +import type { LocatorPublic } from '@kbn/share-plugin/public'; +import type { MapsAppLocator } from '../map_locator/types'; + +export interface MapsAppRegionMapLocatorParams extends SerializableRecord { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + termsSize?: number; + colorSchema: string; + indexPatternId?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; +} + +export type MapsAppRegionMapLocator = LocatorPublic; + +export interface MapsAppRegionMapLocatorDependencies { + locator: MapsAppLocator; +} diff --git a/x-pack/plugins/maps/public/locators/tile_map_locator/get_location.ts b/x-pack/plugins/maps/public/locators/tile_map_locator/get_location.ts new file mode 100644 index 0000000000000..fb7bb0b7d3bd3 --- /dev/null +++ b/x-pack/plugins/maps/public/locators/tile_map_locator/get_location.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { LayerDescriptor } from '../../../common/descriptor_types'; +import { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor'; +import type { MapsAppTileMapLocatorParams, MapsAppTileMapLocatorDependencies } from './types'; + +export async function getLocation( + params: MapsAppTileMapLocatorParams, + deps: MapsAppTileMapLocatorDependencies +) { + const { + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + filters, + query, + timeRange, + hash = true, + } = params; + const initialLayers = [] as unknown as LayerDescriptor[] & SerializableRecord; + const tileMapLayerDescriptor = createTileMapLayerDescriptor({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + }); + + if (tileMapLayerDescriptor) { + initialLayers.push(tileMapLayerDescriptor); + } + + return await deps.locator.getLocation({ + initialLayers, + filters, + query, + timeRange, + hash, + }); +} diff --git a/x-pack/plugins/maps/public/locators/tile_map_locator/locator_definition.ts b/x-pack/plugins/maps/public/locators/tile_map_locator/locator_definition.ts new file mode 100644 index 0000000000000..e511d641fc5ae --- /dev/null +++ b/x-pack/plugins/maps/public/locators/tile_map_locator/locator_definition.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition } from '@kbn/share-plugin/public'; +import type { MapsAppTileMapLocatorParams, MapsAppTileMapLocatorDependencies } from './types'; + +export const MAPS_APP_TILE_MAP_LOCATOR = 'MAPS_APP_TILE_MAP_LOCATOR' as const; + +export class MapsAppTileMapLocatorDefinition + implements LocatorDefinition +{ + public readonly id = MAPS_APP_TILE_MAP_LOCATOR; + + constructor(protected readonly deps: MapsAppTileMapLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppTileMapLocatorParams) => { + const { getLocation } = await import('./get_location'); + return getLocation(params, this.deps); + }; +} diff --git a/x-pack/plugins/maps/public/locators/tile_map_locator/types.ts b/x-pack/plugins/maps/public/locators/tile_map_locator/types.ts new file mode 100644 index 0000000000000..5743feca993d6 --- /dev/null +++ b/x-pack/plugins/maps/public/locators/tile_map_locator/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import type { Filter, TimeRange, Query } from '@kbn/es-query'; +import type { LocatorPublic } from '@kbn/share-plugin/public'; +import type { MapsAppLocator } from '../map_locator/types'; + +export interface MapsAppTileMapLocatorParams extends SerializableRecord { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; +} + +export type MapsAppTileMapLocator = LocatorPublic; + +export interface MapsAppTileMapLocatorDependencies { + locator: MapsAppLocator; +} diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index b93c5421f5cb3..5a4d3e8cf0038 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -10,8 +10,7 @@ import type { ResolvedSimpleSavedObject } from '@kbn/core/public'; import { AttributeService } from '@kbn/embeddable-plugin/public'; import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; import type { MapAttributes } from '../common/content_management'; -import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -import { getMapEmbeddableDisplayName } from '../common/i18n_getters'; +import { MAP_EMBEDDABLE_NAME, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getCoreOverlays, getEmbeddableService } from './kibana_services'; import { extractReferences, injectReferences } from '../common/migrations/references'; import { mapsClient, checkForDuplicateTitle } from './content_management'; @@ -108,7 +107,7 @@ export function getMapAttributeService(): MapAttributeService { copyOnSave: false, lastSavedTitle: '', isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed, - getDisplayName: getMapEmbeddableDisplayName, + getDisplayName: () => MAP_EMBEDDABLE_NAME, onTitleDuplicate: props.onTitleDuplicate, }, { diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 0d558699e6e51..75c4211c54d58 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -56,32 +56,29 @@ import { tileMapRenderer, tileMapVisType, } from './legacy_visualizations'; -import { - MapsAppLocatorDefinition, - MapsAppRegionMapLocatorDefinition, - MapsAppTileMapLocatorDefinition, -} from './locators'; +import { MapsAppLocatorDefinition } from './locators/map_locator/locator_definition'; +import { MapsAppTileMapLocatorDefinition } from './locators/tile_map_locator/locator_definition'; +import { MapsAppRegionMapLocatorDefinition } from './locators/region_map_locator/locator_definition'; import { registerLicensedFeatures, setLicensingPluginStart } from './licensed_features'; import { registerSource } from './classes/sources/source_registry'; -import { registerLayerWizardExternal } from './classes/layers'; +import { registerLayerWizardExternal } from './classes/layers/wizards/layer_wizard_registry'; import { createLayerDescriptors, MapsSetupApi, MapsStartApi, suggestEMSTermJoinConfig, } from './api'; -import { lazyLoadMapModules } from './lazy_load_bundle'; -import { getAppTitle } from '../common/i18n_getters'; import { MapsXPackConfig, MapsConfigType } from '../config'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; -import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent_action'; -import { synchronizeMovementAction } from './trigger_actions/synchronize_movement_action'; +import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent/action'; +import { synchronizeMovementAction } from './trigger_actions/synchronize_movement/action'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; -import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { APP_NAME, APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_services'; -import { MapInspectorView, VectorTileInspectorView } from './inspector'; +import { MapInspectorView } from './inspector/map_adapter/map_inspector_view'; +import { VectorTileInspectorView } from './inspector/vector_tile_adapter/vector_tile_inspector_view'; import { setupLensChoroplethChart } from './lens'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; @@ -193,7 +190,7 @@ export class MapsPlugin core.application.register({ id: APP_ID, - title: getAppTitle(), + title: APP_NAME, order: 4000, icon: `plugins/${APP_ID}/icon.svg`, euiIconType: APP_ICON_SOLUTION, @@ -202,7 +199,7 @@ export class MapsPlugin const [coreStart, { savedObjectsTagging }] = await core.getStartServices(); const UsageTracker = plugins.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; - const { renderApp } = await lazyLoadMapModules(); + const { renderApp } = await import('./render_app'); return renderApp(params, { coreStart, AppUsageTracker: UsageTracker, savedObjectsTagging }); }, }); @@ -212,7 +209,7 @@ export class MapsPlugin version: { latest: LATEST_VERSION, }, - name: getAppTitle(), + name: APP_NAME, }); setupLensChoroplethChart(core, plugins.expressions, plugins.lens); diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index e1ce3e801aac3..95063f728a8fc 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -13,7 +13,7 @@ import { TableListView } from '@kbn/content-management-table-list'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list'; import type { MapItem } from '../../../common/content_management'; -import { APP_ID, getEditPath, MAP_PATH } from '../../../common/constants'; +import { APP_ID, APP_NAME, getEditPath, MAP_PATH } from '../../../common/constants'; import { getMapsCapabilities, getCoreChrome, @@ -22,7 +22,6 @@ import { getUiSettings, getUsageCollection, } from '../../kibana_services'; -import { getAppTitle } from '../../../common/i18n_getters'; import { mapsClient } from '../../content_management'; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; @@ -73,8 +72,8 @@ function MapsListViewComp({ history }: Props) { const listingLimit = getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = getUiSettings().get(SAVED_OBJECTS_PER_PAGE_SETTING); - getCoreChrome().docTitle.change(getAppTitle()); - getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); + getCoreChrome().docTitle.change(APP_NAME); + getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]); const findMaps = useCallback( async ( @@ -128,7 +127,7 @@ function MapsListViewComp({ history }: Props) { entityNamePlural={i18n.translate('xpack.maps.mapListing.entityNamePlural', { defaultMessage: 'maps', })} - tableListTitle={getAppTitle()} + tableListTitle={APP_NAME} onClickTitle={({ id }) => history.push(getEditPath(id))} /> ); diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 4220e25212f2a..ceb9a487fffdc 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -47,8 +47,12 @@ import { AppStateManager, startAppStateSyncing } from '../url_state'; import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from '../top_nav_config'; -import { getEditPath, getFullPath, APP_ID } from '../../../../common/constants'; -import { getMapEmbeddableDisplayName } from '../../../../common/i18n_getters'; +import { + getEditPath, + getFullPath, + APP_ID, + MAP_EMBEDDABLE_NAME, +} from '../../../../common/constants'; import { getInitialQuery, getInitialRefreshConfig, @@ -432,7 +436,7 @@ export class MapApp extends React.Component { await spaces.ui.redirectLegacyUrl({ path: newPath, aliasPurpose: sharingSavedObjectProps.aliasPurpose, - objectNoun: getMapEmbeddableDisplayName(), + objectNoun: MAP_EMBEDDABLE_NAME, }); return; } @@ -547,7 +551,7 @@ export class MapApp extends React.Component { const spaces = getSpacesApi(); return spaces && sharingSavedObjectProps?.outcome === 'conflict' ? spaces.ui.components.getLegacyUrlConflict({ - objectNoun: getMapEmbeddableDisplayName(), + objectNoun: MAP_EMBEDDABLE_NAME, currentObjectId: this.props.savedMap.getSavedObjectId()!, otherObjectId: sharingSavedObjectProps.aliasTargetId!, otherObjectPath: `${getEditPath(sharingSavedObjectProps.aliasTargetId!)}${ diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_breadcrumbs.tsx index ca3022043cd9f..344722a480b08 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_breadcrumbs.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ScopedHistory } from '@kbn/core/public'; import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services'; -import { getAppTitle } from '../../../../common/i18n_getters'; +import { APP_NAME } from '../../../../common/constants'; export const unsavedChangesWarning = i18n.translate( 'xpack.maps.breadCrumbs.unsavedChangesWarning', @@ -49,7 +49,7 @@ export function getBreadcrumbs({ if (!isByValue) { breadcrumbs.push({ - text: getAppTitle(), + text: APP_NAME, onClick: async () => { if (getHasUnsavedChanges()) { const confirmed = await getCoreOverlays().openConfirm(unsavedChangesWarning, { diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 8b7efa9335858..6bef5987dd9f8 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -27,8 +27,8 @@ import { getSavedObjectsTagging, getPresentationUtilContext, } from '../../kibana_services'; +import { MAP_EMBEDDABLE_NAME } from '../../../common/constants'; import { SavedMap } from './saved_map'; -import { getMapEmbeddableDisplayName } from '../../../common/i18n_getters'; import { checkForDuplicateTitle } from '../../content_management'; const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); @@ -179,7 +179,7 @@ export function getTopNavConfig({ copyOnSave: props.newCopyOnSave, lastSavedTitle: savedMap.getSavedObjectId() ? savedMap.getTitle() : '', isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed, - getDisplayName: getMapEmbeddableDisplayName, + getDisplayName: () => MAP_EMBEDDABLE_NAME, onTitleDuplicate: props.onTitleDuplicate, }, { diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.tsx b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/action.ts similarity index 63% rename from x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.tsx rename to x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/action.ts index c2183774cf677..5e21c28d69fe1 100644 --- a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.tsx +++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/action.ts @@ -5,25 +5,13 @@ * 2.0. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { createReactOverlays } from '@kbn/kibana-react-plugin/public'; +import type { Embeddable } from '@kbn/embeddable-plugin/public'; import { createAction } from '@kbn/ui-actions-plugin/public'; -import { isLegacyMap } from '../legacy_visualizations'; -import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -import { getCore } from '../kibana_services'; +import type { FilterByMapExtentActionContext, FilterByMapExtentInput } from './types'; export const FILTER_BY_MAP_EXTENT = 'FILTER_BY_MAP_EXTENT'; -interface FilterByMapExtentInput extends EmbeddableInput { - filterByMapExtent: boolean; -} - -interface FilterByMapExtentActionContext { - embeddable: Embeddable; -} - function getContainerLabel(embeddable: Embeddable) { return embeddable.parent?.type === 'dashboard' ? i18n.translate('xpack.maps.filterByMapExtentMenuItem.dashboardLabel', { @@ -58,20 +46,12 @@ export const filterByMapExtentAction = createAction { return 'filter'; }, - isCompatible: async ({ embeddable }: FilterByMapExtentActionContext) => { - return ( - (embeddable.type === MAP_SAVED_OBJECT_TYPE || isLegacyMap(embeddable)) && - !embeddable.getInput().disableTriggers - ); + isCompatible: async (context: FilterByMapExtentActionContext) => { + const { isCompatible } = await import('./is_compatible'); + return isCompatible(context); }, execute: async (context: FilterByMapExtentActionContext) => { - const { FilterByMapExtentModal } = await import('./filter_by_map_extent_modal'); - const { openModal } = createReactOverlays(getCore()); - const modalSession = openModal( - modalSession.close()} - title={getDisplayName(context.embeddable)} - /> - ); + const { openModal } = await import('./modal'); + openModal(getDisplayName(context.embeddable)); }, }); diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/is_compatible.ts b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/is_compatible.ts new file mode 100644 index 0000000000000..32892b6fdc18b --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/is_compatible.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { isLegacyMap } from '../../legacy_visualizations/is_legacy_map'; +import type { FilterByMapExtentActionContext } from './types'; + +export function isCompatible({ embeddable }: FilterByMapExtentActionContext) { + return ( + (embeddable.type === MAP_SAVED_OBJECT_TYPE || isLegacyMap(embeddable)) && + !embeddable.getInput().disableTriggers + ); +} diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_modal.tsx b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/modal.tsx similarity index 78% rename from x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_modal.tsx rename to x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/modal.tsx index 206f754ae65bb..9e5127f9329ba 100644 --- a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_modal.tsx +++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/modal.tsx @@ -14,14 +14,23 @@ import { EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; -import { mapEmbeddablesSingleton } from '../embeddable/map_embeddables_singleton'; +import { createReactOverlays } from '@kbn/kibana-react-plugin/public'; +import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton'; +import { getCore } from '../../kibana_services'; + +export function openModal(title: string) { + const { openModal: reactOverlaysOpenModal } = createReactOverlays(getCore()); + const modalSession = reactOverlaysOpenModal( + modalSession.close()} title={title} /> + ); +} interface Props { onClose: () => void; title: string; } -export class FilterByMapExtentModal extends Component { +class FilterByMapExtentModal extends Component { _renderSwitches() { return mapEmbeddablesSingleton.getMapPanels().map((mapPanel) => { return ( diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/types.ts b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/types.ts new file mode 100644 index 0000000000000..5587588b60730 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; + +export interface FilterByMapExtentInput extends EmbeddableInput { + filterByMapExtent: boolean; +} + +export interface FilterByMapExtentActionContext { + embeddable: Embeddable; +} diff --git a/x-pack/plugins/maps/public/trigger_actions/get_maps_link.ts b/x-pack/plugins/maps/public/trigger_actions/get_maps_link.ts new file mode 100644 index 0000000000000..da0e8bac59235 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/get_maps_link.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { Query } from '@kbn/es-query'; +import type { SerializableRecord } from '@kbn/utility-types'; +import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; +import { getIndexPatternService, getData, getShareService } from '../kibana_services'; +import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../../common/constants'; +import type { LayerDescriptor } from '../../common/descriptor_types'; +import type { MapsAppLocator } from '../locators/map_locator/types'; +import { MAPS_APP_LOCATOR } from '../locators/map_locator/locator_definition'; + +export const getMapsLink = async (context: VisualizeFieldContext) => { + const dataView = await getIndexPatternService().get(context.dataViewSpec.id!); + // create initial layer descriptor + const hasTooltips = + context?.contextualFields?.length && context?.contextualFields[0] !== '_source'; + const initialLayers = [ + { + id: uuidv4(), + visible: true, + type: LAYER_TYPE.MVT_VECTOR, + sourceDescriptor: { + id: uuidv4(), + type: SOURCE_TYPES.ES_SEARCH, + tooltipProperties: hasTooltips ? context.contextualFields : [], + label: dataView.getIndexPattern(), + indexPatternId: context.dataViewSpec.id, + geoField: context.fieldName, + scalingType: SCALING_TYPES.MVT, + }, + }, + ]; + + const locator = getShareService().url.locators.get(MAPS_APP_LOCATOR) as MapsAppLocator; + const location = await locator.getLocation({ + filters: getData().query.filterManager.getFilters(), + query: getData().query.queryString.getQuery() as Query, + initialLayers: initialLayers as unknown as LayerDescriptor[] & SerializableRecord, + timeRange: getData().query.timefilter.timefilter.getTime(), + dataViewSpec: context.dataViewSpec, + }); + + return location; +}; diff --git a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/action.ts b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/action.ts new file mode 100644 index 0000000000000..3a3fd78072865 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/action.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { SynchronizeMovementActionContext } from './types'; + +export const SYNCHRONIZE_MOVEMENT_ACTION = 'SYNCHRONIZE_MOVEMENT_ACTION'; + +export const synchronizeMovementAction = createAction({ + id: SYNCHRONIZE_MOVEMENT_ACTION, + type: SYNCHRONIZE_MOVEMENT_ACTION, + order: 21, + getDisplayName: ({ embeddable }: SynchronizeMovementActionContext) => { + return i18n.translate('xpack.maps.synchronizeMovementAction.title', { + defaultMessage: 'Synchronize map movement', + }); + }, + getDisplayNameTooltip: () => { + return i18n.translate('xpack.maps.synchronizeMovementAction.tooltipContent', { + defaultMessage: + 'Synchronize maps, so that if you zoom and pan in one map, the movement is reflected in other maps', + }); + }, + getIconType: () => { + return 'crosshairs'; + }, + isCompatible: async (context: SynchronizeMovementActionContext) => { + const { isCompatible } = await import('./is_compatible'); + return isCompatible(context); + }, + execute: async (context: SynchronizeMovementActionContext) => { + const { openModal } = await import('./modal'); + openModal(); + }, +}); diff --git a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/is_compatible.ts b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/is_compatible.ts new file mode 100644 index 0000000000000..ef73f8bb23d11 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/is_compatible.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { isLegacyMap } from '../../legacy_visualizations/is_legacy_map'; +import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton'; +import type { SynchronizeMovementActionContext } from './types'; + +export function isCompatible({ embeddable }: SynchronizeMovementActionContext) { + if (!mapEmbeddablesSingleton.hasMultipleMaps()) { + return false; + } + + if ( + embeddable.type === 'lens' && + typeof (embeddable as LensEmbeddable).getSavedVis === 'function' && + (embeddable as LensEmbeddable).getSavedVis()?.visualizationType === 'lnsChoropleth' + ) { + return true; + } + + if (isLegacyMap(embeddable)) { + return true; + } + + return embeddable.type === MAP_SAVED_OBJECT_TYPE; +} diff --git a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement_modal.tsx b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/modal.tsx similarity index 84% rename from x-pack/plugins/maps/public/trigger_actions/synchronize_movement_modal.tsx rename to x-pack/plugins/maps/public/trigger_actions/synchronize_movement/modal.tsx index c6a1dae7eb36b..fa3ad5bfa8031 100644 --- a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement_modal.tsx +++ b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/modal.tsx @@ -15,13 +15,22 @@ import { EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; -import { mapEmbeddablesSingleton } from '../embeddable/map_embeddables_singleton'; +import { createReactOverlays } from '@kbn/kibana-react-plugin/public'; +import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton'; +import { getCore } from '../../kibana_services'; + +export function openModal() { + const { openModal: reactOverlaysOpenModal } = createReactOverlays(getCore()); + const modalSession = reactOverlaysOpenModal( + modalSession.close()} /> + ); +} interface Props { onClose: () => void; } -export class SynchronizeMovementModal extends Component { +class SynchronizeMovementModal extends Component { _renderSwitches() { const mapPanels = mapEmbeddablesSingleton.getMapPanels(); diff --git a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/types.ts b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/types.ts new file mode 100644 index 0000000000000..8b0060ab1efe6 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; + +export interface SynchronizeMovementActionContext { + embeddable: Embeddable; +} diff --git a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement_action.tsx b/x-pack/plugins/maps/public/trigger_actions/synchronize_movement_action.tsx deleted file mode 100644 index 7116e55fd521d..0000000000000 --- a/x-pack/plugins/maps/public/trigger_actions/synchronize_movement_action.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { createReactOverlays } from '@kbn/kibana-react-plugin/public'; -import { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { createAction } from '@kbn/ui-actions-plugin/public'; -import type { Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public'; -import { isLegacyMap } from '../legacy_visualizations'; -import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -import { getCore } from '../kibana_services'; - -export const SYNCHRONIZE_MOVEMENT_ACTION = 'SYNCHRONIZE_MOVEMENT_ACTION'; - -interface SynchronizeMovementActionContext { - embeddable: Embeddable; -} - -export const synchronizeMovementAction = createAction({ - id: SYNCHRONIZE_MOVEMENT_ACTION, - type: SYNCHRONIZE_MOVEMENT_ACTION, - order: 21, - getDisplayName: ({ embeddable }: SynchronizeMovementActionContext) => { - return i18n.translate('xpack.maps.synchronizeMovementAction.title', { - defaultMessage: 'Synchronize map movement', - }); - }, - getDisplayNameTooltip: () => { - return i18n.translate('xpack.maps.synchronizeMovementAction.tooltipContent', { - defaultMessage: - 'Synchronize maps, so that if you zoom and pan in one map, the movement is reflected in other maps', - }); - }, - getIconType: () => { - return 'crosshairs'; - }, - isCompatible: async ({ embeddable }: SynchronizeMovementActionContext) => { - const { mapEmbeddablesSingleton } = await import('../embeddable/map_embeddables_singleton'); - if (!mapEmbeddablesSingleton.hasMultipleMaps()) { - return false; - } - - if ( - embeddable.type === 'lens' && - typeof (embeddable as LensEmbeddable).getSavedVis === 'function' && - (embeddable as LensEmbeddable).getSavedVis()?.visualizationType === 'lnsChoropleth' - ) { - return true; - } - - if (isLegacyMap(embeddable)) { - return true; - } - - return embeddable.type === MAP_SAVED_OBJECT_TYPE; - }, - execute: async ({ embeddable }: SynchronizeMovementActionContext) => { - const { SynchronizeMovementModal } = await import('./synchronize_movement_modal'); - const { openModal } = createReactOverlays(getCore()); - const modalSession = openModal( - modalSession.close()} /> - ); - }, -}); diff --git a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts index c1194fec094a3..417ff0d0bf1fe 100644 --- a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts +++ b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import { i18n } from '@kbn/i18n'; -import type { Query } from '@kbn/es-query'; -import type { SerializableRecord } from '@kbn/utility-types'; import { METRIC_TYPE } from '@kbn/analytics'; import { createAction, @@ -18,16 +15,7 @@ import { import { getUsageCollection } from '../kibana_services'; import { APP_ID } from '../../common/constants'; -import { - getVisualizeCapabilities, - getIndexPatternService, - getData, - getShareService, - getCore, -} from '../kibana_services'; -import { MapsAppLocator, MAPS_APP_LOCATOR } from '../locators'; -import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../../common/constants'; -import { LayerDescriptor } from '../../common/descriptor_types'; +import { getVisualizeCapabilities, getCore } from '../kibana_services'; export const visualizeGeoFieldAction = createAction({ id: ACTION_VISUALIZE_GEO_FIELD, @@ -38,6 +26,7 @@ export const visualizeGeoFieldAction = createAction({ }), isCompatible: async () => !!getVisualizeCapabilities().show, getHref: async (context) => { + const { getMapsLink } = await import('./get_maps_link'); const { app, path } = await getMapsLink(context); return getCore().application.getUrlForApp(app, { @@ -46,6 +35,7 @@ export const visualizeGeoFieldAction = createAction({ }); }, execute: async (context) => { + const { getMapsLink } = await import('./get_maps_link'); const { app, path, state } = await getMapsLink(context); const usageCollection = getUsageCollection(); @@ -61,37 +51,3 @@ export const visualizeGeoFieldAction = createAction({ }); }, }); - -const getMapsLink = async (context: VisualizeFieldContext) => { - const dataView = await getIndexPatternService().get(context.dataViewSpec.id!); - // create initial layer descriptor - const hasTooltips = - context?.contextualFields?.length && context?.contextualFields[0] !== '_source'; - const initialLayers = [ - { - id: uuidv4(), - visible: true, - type: LAYER_TYPE.MVT_VECTOR, - sourceDescriptor: { - id: uuidv4(), - type: SOURCE_TYPES.ES_SEARCH, - tooltipProperties: hasTooltips ? context.contextualFields : [], - label: dataView.getIndexPattern(), - indexPatternId: context.dataViewSpec.id, - geoField: context.fieldName, - scalingType: SCALING_TYPES.MVT, - }, - }, - ]; - - const locator = getShareService().url.locators.get(MAPS_APP_LOCATOR) as MapsAppLocator; - const location = await locator.getLocation({ - filters: getData().query.filterManager.getFilters(), - query: getData().query.queryString.getQuery() as Query, - initialLayers: initialLayers as unknown as LayerDescriptor[] & SerializableRecord, - timeRange: getData().query.timefilter.timefilter.getTime(), - dataViewSpec: context.dataViewSpec, - }); - - return location; -}; diff --git a/x-pack/plugins/maps/public/util.ts b/x-pack/plugins/maps/public/util.ts index 364f72c0b564e..bed83da7e8335 100644 --- a/x-pack/plugins/maps/public/util.ts +++ b/x-pack/plugins/maps/public/util.ts @@ -6,11 +6,12 @@ */ import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client'; -import { getTilemap, getEMSSettings, getMapsEmsStart } from './kibana_services'; +import { getEMSSettings, getMapsEmsStart } from './kibana_services'; import { getLicenseId } from './licensed_features'; export function getKibanaTileMap(): unknown { - return getTilemap(); + const mapsEms = getMapsEmsStart(); + return mapsEms.config.tilemap ? mapsEms.config.tilemap : {}; } export async function getEmsFileLayers(): Promise { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3756275f49b70..21137b5344e2e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20422,7 +20422,6 @@ "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "Changer de calque", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "Annuler", "xpack.maps.aggs.defaultCountLabel": "compte", - "xpack.maps.appTitle": "Cartes", "xpack.maps.attribution.addBtnAriaLabel": "Ajouter une attribution", "xpack.maps.attribution.addBtnLabel": "Ajouter une attribution", "xpack.maps.attribution.applyBtnLabel": "Appliquer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f209a10ee17fa..74b4c94ef5d1c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20422,7 +20422,6 @@ "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "レイヤーを変更", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.aggs.defaultCountLabel": "カウント", - "xpack.maps.appTitle": "マップ", "xpack.maps.attribution.addBtnAriaLabel": "属性を追加", "xpack.maps.attribution.addBtnLabel": "属性を追加", "xpack.maps.attribution.applyBtnLabel": "適用", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8543c48afec38..d78366da8b8c7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20422,7 +20422,6 @@ "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改图层", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "取消", "xpack.maps.aggs.defaultCountLabel": "计数", - "xpack.maps.appTitle": "Maps", "xpack.maps.attribution.addBtnAriaLabel": "添加归因", "xpack.maps.attribution.addBtnLabel": "添加归因", "xpack.maps.attribution.applyBtnLabel": "应用", From c76a68bd899508dc54e313bdf0c73a133c742d24 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:56:57 +0200 Subject: [PATCH 022/100] Make TaskManager health status error/warning reasons more visible (#154045) Resolves: #152289 With this PR we make some of the `debug` logs `warn` and return the message to the health API as reason to add in the status summary message. --- .../lib/calculate_health_status.mock.ts | 14 ----- .../server/lib/calculate_health_status.ts | 24 +++++--- .../server/lib/log_health_metrics.test.ts | 40 +++++++++---- .../server/lib/log_health_metrics.ts | 9 +-- .../server/monitoring/capacity_estimation.ts | 24 ++++---- .../monitoring/monitoring_stats_stream.ts | 1 + .../monitoring/task_run_statistics.test.ts | 12 ++-- .../server/monitoring/task_run_statistics.ts | 4 +- .../task_manager/server/routes/health.test.ts | 59 ++++++++++++------- .../task_manager/server/routes/health.ts | 24 +++++--- 10 files changed, 126 insertions(+), 85 deletions(-) delete mode 100644 x-pack/plugins/task_manager/server/lib/calculate_health_status.mock.ts diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.mock.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.mock.ts deleted file mode 100644 index f34a26560133b..0000000000000 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const createCalculateHealthStatusMock = () => { - return jest.fn(); -}; - -export const calculateHealthStatusMock = { - create: createCalculateHealthStatusMock, -}; diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts index 8bd7f5de80810..d900e66360820 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts @@ -16,7 +16,7 @@ export function calculateHealthStatus( config: TaskManagerConfig, shouldRunTasks: boolean, logger: Logger -): HealthStatus { +): { status: HealthStatus; reason?: string } { const now = Date.now(); // if "hot" health stats are any more stale than monitored_stats_required_freshness @@ -28,27 +28,35 @@ export function calculateHealthStatus( const requiredColdStatsFreshness: number = config.monitored_aggregated_stats_refresh_rate * 1.5; if (hasStatus(summarizedStats.stats, HealthStatus.Error)) { - return HealthStatus.Error; + return { + status: HealthStatus.Error, + reason: summarizedStats.stats.capacity_estimation?.reason, + }; } // Hot timestamps look at runtime stats which are not available when tasks are not running if (shouldRunTasks) { if (hasExpiredHotTimestamps(summarizedStats, now, requiredHotStatsFreshness)) { - logger.debug('setting HealthStatus.Error because of expired hot timestamps'); - return HealthStatus.Error; + const reason = 'setting HealthStatus.Error because of expired hot timestamps'; + logger.warn(reason); + return { status: HealthStatus.Error, reason }; } } if (hasExpiredColdTimestamps(summarizedStats, now, requiredColdStatsFreshness)) { - logger.debug('setting HealthStatus.Error because of expired cold timestamps'); - return HealthStatus.Error; + const reason = 'setting HealthStatus.Error because of expired cold timestamps'; + logger.warn(reason); + return { status: HealthStatus.Error, reason }; } if (hasStatus(summarizedStats.stats, HealthStatus.Warning)) { - return HealthStatus.Warning; + return { + status: HealthStatus.Warning, + reason: summarizedStats.stats.capacity_estimation?.reason, + }; } - return HealthStatus.OK; + return { status: HealthStatus.OK }; } function hasStatus(stats: RawMonitoringStats['stats'], status: HealthStatus): boolean { diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts index 152f0ae82543e..552648e407fa6 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts @@ -41,16 +41,30 @@ describe('logHealthMetrics', () => { const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); // We must change from OK to Warning - (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.OK, + })); logHealthMetrics(health, logger, config, true, docLinks); - (calculateHealthStatus as jest.Mock).mockImplementation( - () => HealthStatus.Warning - ); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.Warning, + })); logHealthMetrics(health, logger, config, true, docLinks); // We must change from OK to Error - (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.OK, + })); logHealthMetrics(health, logger, config, true, docLinks); - (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.Error); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.Error, + })); logHealthMetrics(health, logger, config, true, docLinks); const debugCalls = (logger as jest.Mocked).debug.mock.calls; @@ -175,9 +189,11 @@ describe('logHealthMetrics', () => { }); const health = getMockMonitoredHealth(); const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); - (calculateHealthStatus as jest.Mock).mockImplementation( - () => HealthStatus.Warning - ); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.Warning, + })); logHealthMetrics(health, logger, config, true, docLinks); @@ -201,7 +217,11 @@ describe('logHealthMetrics', () => { }); const health = getMockMonitoredHealth(); const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); - (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.Error); + ( + calculateHealthStatus as jest.Mock<{ status: HealthStatus; reason?: string }> + ).mockImplementation(() => ({ + status: HealthStatus.Error, + })); logHealthMetrics(health, logger, config, true, docLinks); diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts index eff71b28f8abb..bddafb802b41b 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts @@ -40,12 +40,9 @@ export function logHealthMetrics( capacity_estimation: undefined, }, }; - const statusWithoutCapacity = calculateHealthStatus( - healthWithoutCapacity, - config, - shouldRunTasks, - logger - ); + const healthStatus = calculateHealthStatus(healthWithoutCapacity, config, shouldRunTasks, logger); + + const statusWithoutCapacity = healthStatus?.status; if (statusWithoutCapacity === HealthStatus.Warning) { logLevel = LogLevel.Warn; } else if (statusWithoutCapacity === HealthStatus.Error && !isEmpty(monitoredHealth.stats)) { diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts index 88feea306050c..c710d304445e7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -184,13 +184,14 @@ export function estimateCapacity( averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana + averageRecurringRequiredPerMinute / assumedKibanaInstances; - const status = getHealthStatus(logger, { + const { status, reason } = getHealthStatus(logger, { assumedRequiredThroughputPerMinutePerKibana, assumedAverageRecurringRequiredThroughputPerMinutePerKibana, capacityPerMinutePerKibana, }); return { status, + reason, timestamp: new Date().toISOString(), value: { observed: mapValues( @@ -231,27 +232,28 @@ interface GetHealthStatusParams { capacityPerMinutePerKibana: number; } -function getHealthStatus(logger: Logger, params: GetHealthStatusParams): HealthStatus { +function getHealthStatus( + logger: Logger, + params: GetHealthStatusParams +): { status: HealthStatus; reason?: string } { const { assumedRequiredThroughputPerMinutePerKibana, assumedAverageRecurringRequiredThroughputPerMinutePerKibana, capacityPerMinutePerKibana, } = params; if (assumedRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana) { - return HealthStatus.OK; + return { status: HealthStatus.OK }; } if (assumedAverageRecurringRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana) { - logger.debug( - `setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) < capacityPerMinutePerKibana (${capacityPerMinutePerKibana})` - ); - return HealthStatus.Warning; + const reason = `setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) < capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; + logger.warn(reason); + return { status: HealthStatus.Warning, reason }; } - logger.debug( - `setting HealthStatus.Error because assumedRequiredThroughputPerMinutePerKibana (${assumedRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana}) AND assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana})` - ); - return HealthStatus.Error; + const reason = `setting HealthStatus.Error because assumedRequiredThroughputPerMinutePerKibana (${assumedRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana}) AND assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; + logger.warn(reason); + return { status: HealthStatus.Error, reason }; } export function withCapacityEstimate( diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index 19485e41c2ae2..e192a72f7092d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -68,6 +68,7 @@ export interface MonitoredStat { } export type RawMonitoredStat = MonitoredStat & { status: HealthStatus; + reason?: string; }; export interface RawMonitoringStats { diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts index 4d69b23b699b7..0555f75fda278 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts @@ -375,24 +375,24 @@ describe('Task Run Statistics', () => { { Success: 40, RetryScheduled: 40, Failed: 20, status: 'OK' }, ]); - expect(logger.debug).toHaveBeenCalledTimes(5); - expect(logger.debug).toHaveBeenNthCalledWith( + expect(logger.warn).toHaveBeenCalledTimes(5); + expect(logger.warn).toHaveBeenNthCalledWith( 1, 'Health Status warn threshold has been exceeded, resultFrequencySummary.Failed (40) is greater than warn_threshold (39)' ); - expect(logger.debug).toHaveBeenNthCalledWith( + expect(logger.warn).toHaveBeenNthCalledWith( 2, 'Health Status error threshold has been exceeded, resultFrequencySummary.Failed (60) is greater than error_threshold (59)' ); - expect(logger.debug).toHaveBeenNthCalledWith( + expect(logger.warn).toHaveBeenNthCalledWith( 3, 'Health Status error threshold has been exceeded, resultFrequencySummary.Failed (60) is greater than error_threshold (59)' ); - expect(logger.debug).toHaveBeenNthCalledWith( + expect(logger.warn).toHaveBeenNthCalledWith( 4, 'Health Status error threshold has been exceeded, resultFrequencySummary.Failed (60) is greater than error_threshold (59)' ); - expect(logger.debug).toHaveBeenNthCalledWith( + expect(logger.warn).toHaveBeenNthCalledWith( 5, 'Health Status warn threshold has been exceeded, resultFrequencySummary.Failed (40) is greater than warn_threshold (39)' ); diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 0c6063af19286..8dca615f94190 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -433,11 +433,11 @@ function getHealthStatus( ): HealthStatus { if (resultFrequencySummary.Failed > executionErrorThreshold.warn_threshold) { if (resultFrequencySummary.Failed > executionErrorThreshold.error_threshold) { - logger.debug( + logger.warn( `Health Status error threshold has been exceeded, resultFrequencySummary.Failed (${resultFrequencySummary.Failed}) is greater than error_threshold (${executionErrorThreshold.error_threshold})` ); } else { - logger.debug( + logger.warn( `Health Status warn threshold has been exceeded, resultFrequencySummary.Failed (${resultFrequencySummary.Failed}) is greater than warn_threshold (${executionErrorThreshold.warn_threshold})` ); } diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index 6105b7487163c..c5b9add0ce294 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -15,15 +15,9 @@ import { mockHandlerArguments } from './_mock_handler_arguments'; import { sleep } from '../test_utils'; import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { - HealthStatus, - MonitoringStats, - RawMonitoringStats, - summarizeMonitoringStats, -} from '../monitoring'; +import { MonitoringStats, RawMonitoringStats, summarizeMonitoringStats } from '../monitoring'; import { ServiceStatusLevels, Logger } from '@kbn/core/server'; import { configSchema, TaskManagerConfig } from '../config'; -import { calculateHealthStatusMock } from '../lib/calculate_health_status.mock'; import { FillPoolResult } from '../lib/fill_pool'; jest.mock('../lib/log_health_metrics', () => ({ @@ -191,8 +185,6 @@ describe('healthRoute', () => { it('logs the Task Manager stats at a fixed interval', async () => { const router = httpServiceMock.createRouter(); - const calculateHealthStatus = calculateHealthStatusMock.create(); - calculateHealthStatus.mockImplementation(() => HealthStatus.OK); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); const mockStat = mockHealthStats(); @@ -253,8 +245,6 @@ describe('healthRoute', () => { it(`logs at a warn level if the status is warning`, async () => { const router = httpServiceMock.createRouter(); - const calculateHealthStatus = calculateHealthStatusMock.create(); - calculateHealthStatus.mockImplementation(() => HealthStatus.Warning); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); const warnRuntimeStat = mockHealthStats(); @@ -265,7 +255,7 @@ describe('healthRoute', () => { const stats$ = new Subject(); const id = uuidv4(); - healthRoute({ + const { serviceStatus$ } = healthRoute({ router, monitoringStats$: stats$, logger, @@ -287,6 +277,8 @@ describe('healthRoute', () => { docLinks, }); + const serviceStatus = getLatest(serviceStatus$); + stats$.next(warnRuntimeStat); await sleep(1001); stats$.next(warnConfigurationStat); @@ -295,6 +287,12 @@ describe('healthRoute', () => { await sleep(1001); stats$.next(warnEphemeralStat); + expect(await serviceStatus).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: + 'Task Manager is unhealthy - Reason: setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana (78.28472222222223) < capacityPerMinutePerKibana (200)', + }); + expect(logHealthMetrics).toBeCalledTimes(4); expect(logHealthMetrics.mock.calls[0][0]).toMatchObject({ id, @@ -332,8 +330,6 @@ describe('healthRoute', () => { it(`logs at an error level if the status is error`, async () => { const router = httpServiceMock.createRouter(); - const calculateHealthStatus = calculateHealthStatusMock.create(); - calculateHealthStatus.mockImplementation(() => HealthStatus.Error); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); const errorRuntimeStat = mockHealthStats(); @@ -344,7 +340,7 @@ describe('healthRoute', () => { const stats$ = new Subject(); const id = uuidv4(); - healthRoute({ + const { serviceStatus$ } = healthRoute({ router, monitoringStats$: stats$, logger, @@ -366,6 +362,8 @@ describe('healthRoute', () => { docLinks, }); + const serviceStatus = getLatest(serviceStatus$); + stats$.next(errorRuntimeStat); await sleep(1001); stats$.next(errorConfigurationStat); @@ -374,6 +372,12 @@ describe('healthRoute', () => { await sleep(1001); stats$.next(errorEphemeralStat); + expect(await serviceStatus).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: + 'Task Manager is unhealthy - Reason: setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana (78.28472222222223) < capacityPerMinutePerKibana (200)', + }); + expect(logHealthMetrics).toBeCalledTimes(4); expect(logHealthMetrics.mock.calls[0][0]).toMatchObject({ id, @@ -481,12 +485,13 @@ describe('healthRoute', () => { expect(await serviceStatus).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: 'Task Manager is unhealthy', + summary: + 'Task Manager is unhealthy - Reason: setting HealthStatus.Error because of expired hot timestamps', }); - const debugCalls = (logger as jest.Mocked).debug.mock.calls as string[][]; + const warnCalls = (logger as jest.Mocked).warn.mock.calls as string[][]; const warnMessage = /^setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana/; - const found = debugCalls + const found = warnCalls .map((arr) => arr[0]) .find((message) => message.match(warnMessage) != null); expect(found).toMatch(warnMessage); @@ -497,7 +502,7 @@ describe('healthRoute', () => { const stats$ = new Subject(); - healthRoute({ + const { serviceStatus$ } = healthRoute({ router, monitoringStats$: stats$, logger, @@ -514,6 +519,8 @@ describe('healthRoute', () => { docLinks, }); + const serviceStatus = getLatest(serviceStatus$); + await sleep(0); const lastUpdateOfWorkload = new Date(Date.now() - 120000).toISOString(); @@ -533,6 +540,11 @@ describe('healthRoute', () => { await sleep(2000); + expect(await serviceStatus).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: + 'Task Manager is unhealthy - Reason: setting HealthStatus.Error because of expired cold timestamps', + }); expect(await handler(context, req, res)).toMatchObject({ body: { status: 'error', @@ -572,7 +584,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const stats$ = new Subject(); - healthRoute({ + const { serviceStatus$ } = healthRoute({ router, monitoringStats$: stats$, logger, @@ -588,7 +600,7 @@ describe('healthRoute', () => { shouldRunTasks: true, docLinks, }); - + const serviceStatus = getLatest(serviceStatus$); await sleep(0); // eslint-disable-next-line @typescript-eslint/naming-convention @@ -611,6 +623,11 @@ describe('healthRoute', () => { const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); + expect(await serviceStatus).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: + 'Task Manager is unhealthy - Reason: setting HealthStatus.Error because of expired hot timestamps', + }); expect(await handler(context, req, res)).toMatchObject({ body: { status: 'error', diff --git a/x-pack/plugins/task_manager/server/routes/health.ts b/x-pack/plugins/task_manager/server/routes/health.ts index 9ae3cee2bdcf5..7a348147b0b76 100644 --- a/x-pack/plugins/task_manager/server/routes/health.ts +++ b/x-pack/plugins/task_manager/server/routes/health.ts @@ -30,6 +30,7 @@ import { calculateHealthStatus } from '../lib/calculate_health_status'; export type MonitoredHealth = RawMonitoringStats & { id: string; + reason?: string; status: HealthStatus; timestamp: string; }; @@ -87,10 +88,15 @@ export function healthRoute(params: HealthRouteParams): { function getHealthStatus(monitoredStats: MonitoringStats) { const summarizedStats = summarizeMonitoringStats(logger, monitoredStats, config); - const status = calculateHealthStatus(summarizedStats, config, shouldRunTasks, logger); + const { status, reason } = calculateHealthStatus( + summarizedStats, + config, + shouldRunTasks, + logger + ); const now = Date.now(); const timestamp = new Date(now).toISOString(); - return { id: taskManagerId, timestamp, status, ...summarizedStats }; + return { id: taskManagerId, timestamp, status, reason, ...summarizedStats }; } const serviceStatus$: Subject = new Subject(); @@ -106,7 +112,7 @@ export function healthRoute(params: HealthRouteParams): { tap((stats) => { lastMonitoredStats = stats; }), - // Only calculate the summerized stats (calculates all runnign averages and evaluates state) + // Only calculate the summarized stats (calculates all running averages and evaluates state) // when needed by throttling down to the requiredHotStatsFreshness map((stats) => withServiceStatus(getHealthStatus(stats))) ) @@ -174,15 +180,19 @@ export function healthRoute(params: HealthRouteParams): { export function withServiceStatus( monitoredHealth: MonitoredHealth ): [MonitoredHealth, TaskManagerServiceStatus] { + const { reason, status } = monitoredHealth; + const level = - monitoredHealth.status === HealthStatus.OK - ? ServiceStatusLevels.available - : ServiceStatusLevels.degraded; + status === HealthStatus.OK ? ServiceStatusLevels.available : ServiceStatusLevels.degraded; + + const defaultMessage = LEVEL_SUMMARY[level.toString()]; + const summary = reason ? `${defaultMessage} - Reason: ${reason}` : defaultMessage; + return [ monitoredHealth, { level, - summary: LEVEL_SUMMARY[level.toString()], + summary, }, ]; } From dfbb97f307e88c8f94b3c6c85edb0efc86754943 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 18 Apr 2023 16:02:35 +0300 Subject: [PATCH 023/100] [Inspector] Improve the display when there are many columns (#155119) ## Summary Fixes https://github.com/elastic/kibana/issues/112176 Improves the way the inspector table is rendered when there are multiple columns. I just applied the eui team feedback **Now** image **Before** image --- .../__snapshots__/data_view.test.tsx.snap | 8 ++++---- .../components/data_table.tsx | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index f65b08fc632bc..0c60d3b0606be 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -204,7 +204,7 @@ Array [ class="euiSpacer euiSpacer--s emotion-euiSpacer-s" />,
@@ -255,7 +255,7 @@ Array [
@@ -546,7 +546,7 @@ Array [ class="euiSpacer euiSpacer--s emotion-euiSpacer-s" />,
@@ -597,7 +597,7 @@ Array [
diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index 307c074ce5c6f..3deee5fe0bda9 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - +import { css } from '@emotion/react'; import { EuiButtonIcon, EuiFlexGroup, @@ -192,12 +192,25 @@ export class DataTableFormat extends Component div:last-child { + position: sticky; + left: 0; + } + `} /> ); } From c9c31afd8d88d7f0c05831bcd8788a5629e2c1e0 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 18 Apr 2023 09:11:33 -0400 Subject: [PATCH 024/100] chore(slo): improve slo list summary alignment (#154932) --- .../observability/public/data/slo/common.ts | 16 ++++----- .../pages/slos/components/slo_sparkline.tsx | 20 ++++++++--- .../pages/slos/components/slo_summary.tsx | 35 ++++++++++++++----- .../kibana_react.storybook_decorator.tsx | 1 + 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/observability/public/data/slo/common.ts b/x-pack/plugins/observability/public/data/slo/common.ts index 3c0e1b7e49408..ae25d150f350b 100644 --- a/x-pack/plugins/observability/public/data/slo/common.ts +++ b/x-pack/plugins/observability/public/data/slo/common.ts @@ -35,8 +35,8 @@ export const buildHealthySummary = ( sliValue: 0.99872, errorBudget: { initial: 0.02, - consumed: 0.064, - remaining: 0.936, + consumed: 0.0642, + remaining: 0.93623, isEstimated: false, }, ...params, @@ -48,11 +48,11 @@ export const buildViolatedSummary = ( ): SLOWithSummaryResponse['summary'] => { return { status: 'VIOLATED', - sliValue: 0.97, + sliValue: 0.81232, errorBudget: { initial: 0.02, consumed: 1, - remaining: 0, + remaining: -3.1234, isEstimated: false, }, ...params, @@ -80,11 +80,11 @@ export const buildDegradingSummary = ( ): SLOWithSummaryResponse['summary'] => { return { status: 'DEGRADING', - sliValue: 0.97, + sliValue: 0.97982, errorBudget: { - initial: 0.02, - consumed: 0.88, - remaining: 0.12, + initial: 0.01, + consumed: 0.8822, + remaining: 0.1244, isEstimated: true, }, ...params, diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx index 4f34ea594ddee..8b3586554abf8 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { AreaSeries, Chart, Fit, LineSeries, ScaleType, Settings } from '@elastic/charts'; +import { AreaSeries, Axis, Chart, Fit, LineSeries, ScaleType, Settings } from '@elastic/charts'; import React from 'react'; import { EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme'; @@ -36,28 +36,38 @@ export function SloSparkline({ chart, data, id, isLoading, state }: Props) { const color = state === 'error' ? euiTheme.colors.danger : euiTheme.colors.success; const ChartComponent = chart === 'area' ? AreaSeries : LineSeries; + const LineAxisComponent = + chart === 'line' ? ( + + ) : null; if (isLoading) { return ; } return ( - + + {LineAxisComponent} - - - + + + + - - - + + + { if (setting === 'dateFormat') { From 5b1b15af7a9cd478652d996f5e1648ea5cfd4f43 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 18 Apr 2023 15:17:49 +0200 Subject: [PATCH 025/100] [ML] AIOps: Fix race condition where stale url state would reset search bar. (#154885) Fixes an issue there the global state `_g` and app state `_a` would get out of sync and overwrite each other. For example, a click on Refresh in the date picker (global state) could reset the search bar (app state) to empty. The issue was that in `x-pack/packages/ml/url_state/src/url_state.tsx` the `searchString` could become a stale value in `setUrlState`. This PR fixes it by using the approach already used in `usePageUrlState`: The `searchString` is passed on to be stored via `useRef` so that the `setUrlState` setter can always access the most recent value. --- .../ml/url_state/src/url_state.test.tsx | 118 +++++++++++++----- .../packages/ml/url_state/src/url_state.tsx | 15 ++- x-pack/plugins/aiops/public/hooks/use_data.ts | 52 ++++---- .../apps/aiops/explain_log_rate_spikes.ts | 50 +++++--- .../test/functional/apps/aiops/test_data.ts | 21 +++- x-pack/test/functional/apps/aiops/types.ts | 43 +++++-- .../aiops/explain_log_rate_spikes_page.ts | 32 ++++- 7 files changed, 236 insertions(+), 95 deletions(-) diff --git a/x-pack/packages/ml/url_state/src/url_state.test.tsx b/x-pack/packages/ml/url_state/src/url_state.test.tsx index 734c730dd91ba..033ecd77fadf4 100644 --- a/x-pack/packages/ml/url_state/src/url_state.test.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.test.tsx @@ -5,29 +5,18 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { useEffect, type FC } from 'react'; import { render, act } from '@testing-library/react'; -import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; +import { MemoryRouter } from 'react-router-dom'; -const mockHistoryPush = jest.fn(); +import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - push: mockHistoryPush, - }), - useLocation: () => ({ - search: - "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d", - }), -})); +const mockHistoryInitialState = + "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d"; describe('getUrlState', () => { test('properly decode url with _g and _a', () => { - expect( - parseUrlState( - "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!t,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" - ) - ).toEqual({ + expect(parseUrlState(mockHistoryInitialState)).toEqual({ _a: { mlExplorerFilter: {}, mlExplorerSwimlane: { @@ -46,7 +35,7 @@ describe('getUrlState', () => { }, refreshInterval: { display: 'Off', - pause: true, + pause: false, value: 0, }, time: { @@ -61,29 +50,96 @@ describe('getUrlState', () => { }); describe('useUrlState', () => { - beforeEach(() => { - mockHistoryPush.mockClear(); + it('pushes a properly encoded search string to history', () => { + const TestComponent: FC = () => { + const [appState, setAppState] = useUrlState('_a'); + + useEffect(() => { + setAppState(parseUrlState(mockHistoryInitialState)._a); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + +
{JSON.stringify(appState?.query)}
+ + ); + }; + + const { getByText, getByTestId } = render( + + + + + + ); + + expect(getByTestId('appState').innerHTML).toBe( + '{"query_string":{"analyze_wildcard":true,"query":"*"}}' + ); + + act(() => { + getByText('ButtonText').click(); + }); + + expect(getByTestId('appState').innerHTML).toBe('"my-query"'); }); - test('pushes a properly encoded search string to history', () => { + it('updates both _g and _a state successfully', () => { const TestComponent: FC = () => { - const [, setUrlState] = useUrlState('_a'); - return ; + const [globalState, setGlobalState] = useUrlState('_g'); + const [appState, setAppState] = useUrlState('_a'); + + useEffect(() => { + setGlobalState({ time: 'initial time' }); + setAppState({ query: 'initial query' }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + +
{globalState?.time}
+
{appState?.query}
+ + ); }; - const { getByText } = render( - - - + const { getByText, getByTestId } = render( + + + + + ); + expect(getByTestId('globalState').innerHTML).toBe('initial time'); + expect(getByTestId('appState').innerHTML).toBe('initial query'); + act(() => { - getByText('ButtonText').click(); + getByText('GlobalStateButton1').click(); }); - expect(mockHistoryPush).toHaveBeenCalledWith({ - search: - '_a=%28mlExplorerFilter%3A%28%29%2CmlExplorerSwimlane%3A%28viewByFieldName%3Aaction%29%2Cquery%3A%28%29%29&_g=%28ml%3A%28jobIds%3A%21%28dec-2%29%29%2CrefreshInterval%3A%28display%3AOff%2Cpause%3A%21f%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272019-01-01T00%3A03%3A40.000Z%27%2Cmode%3Aabsolute%2Cto%3A%272019-08-30T11%3A55%3A07.000Z%27%29%29&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d', + expect(getByTestId('globalState').innerHTML).toBe('now-15m'); + expect(getByTestId('appState').innerHTML).toBe('initial query'); + + act(() => { + getByText('AppStateButton').click(); + }); + + expect(getByTestId('globalState').innerHTML).toBe('now-15m'); + expect(getByTestId('appState').innerHTML).toBe('the updated query'); + + act(() => { + getByText('GlobalStateButton2').click(); }); + + expect(getByTestId('globalState').innerHTML).toBe('now-5y'); + expect(getByTestId('appState').innerHTML).toBe('the updated query'); }); }); diff --git a/x-pack/packages/ml/url_state/src/url_state.tsx b/x-pack/packages/ml/url_state/src/url_state.tsx index d643a22bde6e4..bd62e1f61029a 100644 --- a/x-pack/packages/ml/url_state/src/url_state.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.tsx @@ -94,6 +94,12 @@ export const UrlStateProvider: FC = ({ children }) => { const history = useHistory(); const { search: searchString } = useLocation(); + const searchStringRef = useRef(searchString); + + useEffect(() => { + searchStringRef.current = searchString; + }, [searchString]); + const setUrlState: SetUrlState = useCallback( ( accessor: Accessor, @@ -101,7 +107,8 @@ export const UrlStateProvider: FC = ({ children }) => { value?: any, replaceState?: boolean ) => { - const prevSearchString = searchString; + const prevSearchString = searchStringRef.current; + const urlState = parseUrlState(prevSearchString); const parsedQueryString = parse(prevSearchString, { sort: false }); @@ -142,6 +149,10 @@ export const UrlStateProvider: FC = ({ children }) => { if (oldLocationSearchString !== newLocationSearchString) { const newSearchString = stringify(parsedQueryString, { sort: false }); + // Another `setUrlState` call could happen before the updated + // `searchString` gets propagated via `useLocation` therefore + // we update the ref right away too. + searchStringRef.current = newSearchString; if (replaceState) { history.replace({ search: newSearchString }); } else { @@ -154,7 +165,7 @@ export const UrlStateProvider: FC = ({ children }) => { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [searchString] + [] ); return {children}; diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index 3546cead3edab..82db675a94c4f 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -45,9 +45,6 @@ export const useData = ( } = useAiopsAppContext(); const [lastRefresh, setLastRefresh] = useState(0); - const [fieldStatsRequest, setFieldStatsRequest] = useState< - DocumentStatsSearchStrategyParams | undefined - >(); /** Prepare required params to pass to search strategy **/ const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { @@ -91,12 +88,30 @@ export const useData = ( ]); const _timeBuckets = useTimeBuckets(); - const timefilter = useTimefilter({ timeRangeSelector: selectedDataView?.timeFieldName !== undefined, autoRefreshSelector: true, }); + const fieldStatsRequest: DocumentStatsSearchStrategyParams | undefined = useMemo(() => { + const timefilterActiveBounds = timefilter.getActiveBounds(); + if (timefilterActiveBounds !== undefined) { + _timeBuckets.setInterval('auto'); + _timeBuckets.setBounds(timefilterActiveBounds); + _timeBuckets.setBarTarget(barTarget); + return { + earliest: timefilterActiveBounds.min?.valueOf(), + latest: timefilterActiveBounds.max?.valueOf(), + intervalMs: _timeBuckets.getInterval()?.asMilliseconds(), + index: selectedDataView.getIndexPattern(), + searchQuery, + timeFieldName: selectedDataView.timeFieldName, + runtimeFieldMap: selectedDataView.getRuntimeMappings(), + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastRefresh, searchQuery]); + const overallStatsRequest = useMemo(() => { return fieldStatsRequest ? { @@ -125,25 +140,6 @@ export const useData = ( lastRefresh ); - function updateFieldStatsRequest() { - const timefilterActiveBounds = timefilter.getActiveBounds(); - if (timefilterActiveBounds !== undefined) { - _timeBuckets.setInterval('auto'); - _timeBuckets.setBounds(timefilterActiveBounds); - _timeBuckets.setBarTarget(barTarget); - setFieldStatsRequest({ - earliest: timefilterActiveBounds.min?.valueOf(), - latest: timefilterActiveBounds.max?.valueOf(), - intervalMs: _timeBuckets.getInterval()?.asMilliseconds(), - index: selectedDataView.getIndexPattern(), - searchQuery, - timeFieldName: selectedDataView.timeFieldName, - runtimeFieldMap: selectedDataView.getRuntimeMappings(), - }); - setLastRefresh(Date.now()); - } - } - useEffect(() => { const timefilterUpdateSubscription = merge( timefilter.getAutoRefreshFetch$(), @@ -156,13 +152,13 @@ export const useData = ( refreshInterval: timefilter.getRefreshInterval(), }); } - updateFieldStatsRequest(); + setLastRefresh(Date.now()); }); // This listens just for an initial update of the timefilter to be switched on. const timefilterEnabledSubscription = timefilter.getEnabledUpdated$().subscribe(() => { if (fieldStatsRequest === undefined) { - updateFieldStatsRequest(); + setLastRefresh(Date.now()); } }); @@ -173,12 +169,6 @@ export const useData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Ensure request is updated when search changes - useEffect(() => { - updateFieldStatsRequest(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchString, JSON.stringify(searchQuery)]); - return { documentStats, timefilter, diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index d34169b05a408..92320dad62087 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -10,7 +10,7 @@ import { orderBy } from 'lodash'; import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; -import type { TestData } from './types'; +import { isTestDataExpectedWithSampleProbability, type TestData } from './types'; import { explainLogRateSpikesTestData } from './test_data'; export default function ({ getPageObject, getService }: FtrProviderContext) { @@ -43,9 +43,21 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.assertTimeRangeSelectorSectionExists(); await ml.testExecution.logTestStep(`${testData.suiteTitle} loads data for full time range`); + if (testData.query) { + await aiops.explainLogRateSpikesPage.setQueryInput(testData.query); + } await aiops.explainLogRateSpikesPage.clickUseFullDataButton( testData.expected.totalDocCountFormatted ); + + if (isTestDataExpectedWithSampleProbability(testData.expected)) { + await aiops.explainLogRateSpikesPage.assertSamplingProbability( + testData.expected.sampleProbabilityFormatted + ); + } else { + await aiops.explainLogRateSpikesPage.assertSamplingProbabilityMissing(); + } + await headerPage.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep( @@ -147,21 +159,24 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesAnalysisGroupsTable.assertSpikeAnalysisTableExists(); - const analysisGroupsTable = - await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); - - expect(orderBy(analysisGroupsTable, 'group')).to.be.eql( - orderBy(testData.expected.analysisGroupsTable, 'group') - ); + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const analysisGroupsTable = + await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); + expect(orderBy(analysisGroupsTable, 'group')).to.be.eql( + orderBy(testData.expected.analysisGroupsTable, 'group') + ); + } await ml.testExecution.logTestStep('expand table row'); await aiops.explainLogRateSpikesAnalysisGroupsTable.assertExpandRowButtonExists(); await aiops.explainLogRateSpikesAnalysisGroupsTable.expandRow(); - const analysisTable = await aiops.explainLogRateSpikesAnalysisTable.parseAnalysisTable(); - expect(orderBy(analysisTable, ['fieldName', 'fieldValue'])).to.be.eql( - orderBy(testData.expected.analysisTable, ['fieldName', 'fieldValue']) - ); + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const analysisTable = await aiops.explainLogRateSpikesAnalysisTable.parseAnalysisTable(); + expect(orderBy(analysisTable, ['fieldName', 'fieldValue'])).to.be.eql( + orderBy(testData.expected.analysisTable, ['fieldName', 'fieldValue']) + ); + } // Assert the field selector that allows to costumize grouping await aiops.explainLogRateSpikesPage.assertFieldFilterPopoverButtonExists(false); @@ -182,11 +197,14 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (testData.fieldSelectorApplyAvailable) { await aiops.explainLogRateSpikesPage.clickFieldFilterApplyButton(); - const filteredAnalysisGroupsTable = - await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); - expect(orderBy(filteredAnalysisGroupsTable, 'group')).to.be.eql( - orderBy(testData.expected.filteredAnalysisGroupsTable, 'group') - ); + + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const filteredAnalysisGroupsTable = + await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); + expect(orderBy(filteredAnalysisGroupsTable, 'group')).to.be.eql( + orderBy(testData.expected.filteredAnalysisGroupsTable, 'group') + ); + } } }); } diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index 1ccc441618bdb..b6d3293aeba81 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -19,6 +19,24 @@ export const farequoteDataViewTestData: TestData = { fieldSelectorApplyAvailable: false, expected: { totalDocCountFormatted: '86,374', + sampleProbabilityFormatted: '0.5', + fieldSelectorPopover: ['airline', 'custom_field.keyword'], + }, +}; + +export const farequoteDataViewTestDataWithQuery: TestData = { + suiteTitle: 'farequote with spike', + dataGenerator: 'farequote_with_spike', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'ft_farequote', + brushDeviationTargetTimestamp: 1455033600000, + brushIntervalFactor: 1, + chartClickCoordinates: [0, 0], + fieldSelectorSearch: 'airline', + fieldSelectorApplyAvailable: false, + query: 'NOT airline:("SWR" OR "ACA" OR "AWE" OR "BAW" OR "JAL" OR "JBU" OR "JZA" OR "KLM")', + expected: { + totalDocCountFormatted: '48,799', analysisGroupsTable: [ { docCount: '297', @@ -34,7 +52,7 @@ export const farequoteDataViewTestData: TestData = { fieldName: 'airline', fieldValue: 'AAL', logRate: 'Chart type:bar chart', - pValue: '4.66e-11', + pValue: '1.18e-8', impact: 'High', }, ], @@ -105,5 +123,6 @@ export const artificialLogDataViewTestData: TestData = { export const explainLogRateSpikesTestData: TestData[] = [ farequoteDataViewTestData, + farequoteDataViewTestDataWithQuery, artificialLogDataViewTestData, ]; diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 7a758aa4a65ff..01733a8e1a2af 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -5,6 +5,34 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +interface TestDataExpectedWithSampleProbability { + totalDocCountFormatted: string; + sampleProbabilityFormatted: string; + fieldSelectorPopover: string[]; +} + +export function isTestDataExpectedWithSampleProbability( + arg: unknown +): arg is TestDataExpectedWithSampleProbability { + return isPopulatedObject(arg, ['sampleProbabilityFormatted']); +} + +interface TestDataExpectedWithoutSampleProbability { + totalDocCountFormatted: string; + analysisGroupsTable: Array<{ group: string; docCount: string }>; + filteredAnalysisGroupsTable?: Array<{ group: string; docCount: string }>; + analysisTable: Array<{ + fieldName: string; + fieldValue: string; + logRate: string; + pValue: string; + impact: string; + }>; + fieldSelectorPopover: string[]; +} + export interface TestData { suiteTitle: string; dataGenerator: string; @@ -17,17 +45,6 @@ export interface TestData { chartClickCoordinates: [number, number]; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; - expected: { - totalDocCountFormatted: string; - analysisGroupsTable: Array<{ group: string; docCount: string }>; - filteredAnalysisGroupsTable?: Array<{ group: string; docCount: string }>; - analysisTable: Array<{ - fieldName: string; - fieldValue: string; - logRate: string; - pValue: string; - impact: string; - }>; - fieldSelectorPopover: string[]; - }; + query?: string; + expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 3a921a74ee359..3da9ed7c760b7 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -9,12 +9,16 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; -export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderContext) { +export function ExplainLogRateSpikesPageProvider({ + getService, + getPageObject, +}: FtrProviderContext) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const ml = getService('ml'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const header = getPageObject('header'); return { async assertTimeRangeSelectorSectionExists() { @@ -31,6 +35,32 @@ export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderCont }); }, + async assertSamplingProbability(expectedFormattedSamplingProbability: string) { + await retry.tryForTime(5000, async () => { + const samplingProbability = await testSubjects.getVisibleText('aiopsSamplingProbability'); + expect(samplingProbability).to.eql( + expectedFormattedSamplingProbability, + `Expected total document count to be '${expectedFormattedSamplingProbability}' (got '${samplingProbability}')` + ); + }); + }, + + async setQueryInput(query: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + await aiopsQueryInput.type(query); + await aiopsQueryInput.pressKeys(browser.keys.ENTER); + await header.waitUntilLoadingHasFinished(); + const queryBarText = await aiopsQueryInput.getVisibleText(); + expect(queryBarText).to.eql( + query, + `Expected query bar text to be '${query}' (got '${queryBarText}')` + ); + }, + + async assertSamplingProbabilityMissing() { + await testSubjects.missingOrFail('aiopsSamplingProbability'); + }, + async clickUseFullDataButton(expectedFormattedTotalDocCount: string) { await retry.tryForTime(30 * 1000, async () => { await testSubjects.clickWhenNotDisabledWithoutRetry('mlDatePickerButtonUseFullData'); From ebe278490f43152e93f7bf300a1cf65878504891 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 18 Apr 2023 07:33:15 -0600 Subject: [PATCH 026/100] [reporting] show loading state when creating a reporting job (#154939) Screen Shot 2023-04-13 at 12 04 26 PM ### Steps to test * Load your favorite sample data set and open its dashboard * Click "Share" and then click "PDF Reports" * Open browser devtools and open network tab. Turn on network throttling to better see loading state * Click "Generate PDF". Notice how button now gives feedback its clicked and something is happening. Before, button would not show loading state and users are confused into thinking nothing is happening. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../reporting_panel_content.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx index 9102802f5d79e..6ac0e374b3919 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx @@ -62,6 +62,7 @@ interface State { absoluteUrl: string; layoutId: string; objectType: string; + isCreatingReportJob: boolean; } class ReportingPanelContentUi extends Component { @@ -78,6 +79,7 @@ class ReportingPanelContentUi extends Component { absoluteUrl: this.getAbsoluteReportGenerationUrl(props), layoutId: '', objectType, + isCreatingReportJob: false, }; } @@ -227,12 +229,13 @@ class ReportingPanelContentUi extends Component { private renderGenerateReportButton = (isDisabled: boolean) => { return ( { this.props.getJobParams() ); + this.setState({ isCreatingReportJob: true }); + return this.props.apiClient .createReportingJob(this.props.reportType, decoratedJobParams) .then(() => { @@ -313,6 +318,9 @@ class ReportingPanelContentUi extends Component { if (this.props.onClose) { this.props.onClose(); } + if (this.mounted) { + this.setState({ isCreatingReportJob: false }); + } }) .catch((error) => { this.props.toasts.addError(error, { @@ -325,6 +333,9 @@ class ReportingPanelContentUi extends Component { ) as unknown as string, }); + if (this.mounted) { + this.setState({ isCreatingReportJob: false }); + } }); }; } From 55b9fd2353b487a3db3997d4313c564e3f3b9853 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 18 Apr 2023 15:39:29 +0200 Subject: [PATCH 027/100] [Dev docs] Added final section to HTTP versioning tutorial (#154901) ## Summary Adds the final section to the HTTP versioning tutorial about using the route versioning specification. --- dev_docs/tutorials/versioning_http_apis.mdx | 117 ++++++++++++++++++-- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/dev_docs/tutorials/versioning_http_apis.mdx b/dev_docs/tutorials/versioning_http_apis.mdx index c8a3625fb4977..81bfed4f4dc4e 100644 --- a/dev_docs/tutorials/versioning_http_apis.mdx +++ b/dev_docs/tutorials/versioning_http_apis.mdx @@ -41,7 +41,7 @@ router.get( ); ``` -#### Why is this problemetic for versioning? +#### Why is this problematic for versioning? Whenever we perform a data migration the body of this endpoint will change for all clients. This prevents us from being able to maintain past interfaces and gracefully introduce new ones. @@ -119,7 +119,7 @@ router.post( } ); ``` -#### Why is this problemetic for versioning? +#### Why is this problematic for versioning? This HTTP API currently accepts all numbers and strings as input which allows for unexpected inputs like negative numbers or non-URL friendly characters. This may break future migrations or integrations that assume your data will always be within certain parameters. @@ -141,7 +141,7 @@ This HTTP API currently accepts all numbers and strings as input which allows fo Adding this validation we negate the risk of unexpected values. It is not necessary to use `@kbn/config-schema`, as long as your validation mechanism provides finer grained controls than "number" or "string". -In summary: think about the acceptable paramaters for every input your HTTP API expects. +In summary: think about the acceptable parameters for every input your HTTP API expects. ### 3. Keep interfaces as "narrow" as possible @@ -170,7 +170,7 @@ router.get( The above code follows guidelines from steps 1 and 2, but it allows clients to specify ANY string by which to sort. This is a far "wider" API than we need for this endpoint. -#### Why is this problemetic for versioning? +#### Why is this problematic for versioning? Without telemetry it is impossible to know what values clients might be passing through — and what type of sort behaviour they are expecting. @@ -207,9 +207,112 @@ router.get( The changes are: -1. New input validation accepts a known set of values. This makes our HTTP API far _narrower_ and specific to our use case. It does not matter that our `sortSchema` has the same values as our persistence schema, what matters is that we created a **translation layer** between our HTTP API and our internal schema. This faclitates easily versioning this endpoint. +1. New input validation accepts a known set of values. This makes our HTTP API far _narrower_ and specific to our use case. It does not matter that our `sortSchema` has the same values as our persistence schema, what matters is that we created a **translation layer** between our HTTP API and our internal schema. This facilitates easily versioning this endpoint. 2. **Bonus point**: we use the `escapeKuery` utility to defend against KQL injection attacks. -### 4. Use the versioned API spec +### 4. Adhere to the HTTP versioning specification + +#### Choosing the right version + +##### Public endpoints +Public endpoints include any endpoint that is intended for users to directly integrate with via HTTP. + +Choose a date string in the format `YYYY-MM-DD`. This date should be the date that a (group) of APIs was made available. + +##### Internal endpoints +Internal endpoints are all non-public endpoints (see definition above). + +If you need to maintain backwards-compatibility for an internal endpoint use a single, larger-than-zero number. Ex. `1`. + + +#### Use the versioned router + +Core exposes a versioned router that ensures your endpoint's behaviour and formatting all conforms to the versioning specification. + +```typescript + router.versioned. + .post({ + access: 'public', // This endpoint is intended for a public audience + path: '/api/my-app/foo/{id?}', + options: { timeout: { payload: 60000 } }, + }) + .addVersion( + { + version: '2023-01-01', // The public version of this API + validate: { + request: { + query: schema.object({ + name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })), + }), + params: schema.object({ + id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })), + }), + body: schema.object({ foo: schema.string() }), + }, + response: { + 200: { // In development environments, this validation will run against 200 responses + body: schema.object({ foo: schema.string() }), + }, + }, + }, + }, + async (ctx, req, res) => { + await ctx.fooService.create(req.body.foo, req.params.id, req.query.name); + return res.ok({ body: { foo: req.body.foo } }); + } + ) + // BREAKING CHANGE: { foo: string } => { fooString: string } in response body + .addVersion( + { + version: '2023-02-01', + validate: { + request: { + query: schema.object({ + name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })), + }), + params: schema.object({ + id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })), + }), + body: schema.object({ fooString: schema.string() }), + }, + response: { + 200: { + body: schema.object({ fooName: schema.string() }), + }, + }, + }, + }, + async (ctx, req, res) => { + await ctx.fooService.create(req.body.fooString, req.params.id, req.query.name); + return res.ok({ body: { fooName: req.body.fooString } }); + } + ) + // BREAKING CHANGES: Enforce min/max length on fooString + .addVersion( + { + version: '2023-03-01', + validate: { + request: { + query: schema.object({ + name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })), + }), + params: schema.object({ + id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })), + }), + body: schema.object({ fooString: schema.string({ minLength: 0, maxLength: 1000 }) }), + }, + response: { + 200: { + body: schema.object({ fooName: schema.string() }), + }, + }, + }, + }, + async (ctx, req, res) => { + await ctx.fooService.create(req.body.fooString, req.params.id, req.query.name); + return res.ok({ body: { fooName: req.body.fooString } }); + } +``` -_Under construction, check back here soon!_ \ No newline at end of file +#### Additional reading +For a more details on the versioning specification see [this document](https://docs.google.com/document/d/1YpF6hXIHZaHvwNaQAxWFzexUF1nbqACTtH2IfDu0ldA/edit?usp=sharing). \ No newline at end of file From b81a2705dfae1b65fef7ad18c0cb470ad83fbf6b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Apr 2023 15:54:11 +0200 Subject: [PATCH 028/100] [Synthetics] Add tooltip for viewer user for edit actions (#155134) --- .../common/components/permissions.tsx | 2 +- .../management/monitor_list_table/columns.tsx | 48 ++++++++++++++----- .../monitor_list_table/monitor_list.tsx | 3 -- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx index 97a9ac3e62ce3..17580afbf2cf4 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx @@ -100,7 +100,7 @@ const CANNOT_PERFORM_ACTION_FLEET = i18n.translate( } ); -const CANNOT_PERFORM_ACTION_SYNTHETICS = i18n.translate( +export const CANNOT_PERFORM_ACTION_SYNTHETICS = i18n.translate( 'xpack.synthetics.monitorManagement.noSyntheticsPermissions', { defaultMessage: 'You do not have sufficient permissions to perform this action.', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index dd56da1e7e563..074695f26c148 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -5,16 +5,20 @@ * 2.0. */ -import { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { FETCH_STATUS } from '@kbn/observability-plugin/public'; +import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities'; import { isStatusEnabled, toggleStatusAlert, } from '../../../../../../../common/runtime_types/monitor_management/alert_config'; -import { NoPermissionsTooltip } from '../../../common/components/permissions'; +import { + CANNOT_PERFORM_ACTION_SYNTHETICS, + NoPermissionsTooltip, +} from '../../../common/components/permissions'; import { TagsBadges } from '../../../common/components/tag_badges'; import { useMonitorAlertEnable } from '../../../../hooks/use_monitor_alert_enable'; import * as labels from './labels'; @@ -35,17 +39,16 @@ import { MonitorEnabled } from './monitor_enabled'; import { MonitorLocations } from './monitor_locations'; export function useMonitorListColumns({ - canEditSynthetics, loading, overviewStatus, setMonitorPendingDeletion, }: { - canEditSynthetics: boolean; loading: boolean; overviewStatus: OverviewStatusState | null; setMonitorPendingDeletion: (config: EncryptedSyntheticsSavedMonitor) => void; }): Array> { const history = useHistory(); + const canEditSynthetics = useCanEditSynthetics(); const { alertStatus, updateAlertEnabledState } = useMonitorAlertEnable(); const { canSaveIntegrations } = useFleetPermissions(); @@ -54,7 +57,7 @@ export function useMonitorListColumns({ return alertStatus(fields[ConfigKey.CONFIG_ID]) === FETCH_STATUS.LOADING; }; - return [ + const columns: Array> = [ { align: 'left' as const, field: ConfigKey.NAME as string, @@ -173,8 +176,8 @@ export function useMonitorListColumns({ ), description: labels.EDIT_LABEL, - icon: 'pencil', - type: 'icon', + icon: 'pencil' as const, + type: 'icon' as const, enabled: (fields) => canEditSynthetics && !isActionLoading(fields) && @@ -197,9 +200,9 @@ export function useMonitorListColumns({ ), description: labels.DELETE_LABEL, - icon: 'trash', - type: 'icon', - color: 'danger', + icon: 'trash' as const, + type: 'icon' as const, + color: 'danger' as const, enabled: (fields) => canEditSynthetics && !isActionLoading(fields) && @@ -216,8 +219,8 @@ export function useMonitorListColumns({ : labels.ENABLE_STATUS_ALERT, icon: (fields) => isStatusEnabled(fields[ConfigKey.ALERT_CONFIG]) ? 'bellSlash' : 'bell', - type: 'icon', - color: 'danger', + type: 'icon' as const, + color: 'danger' as const, enabled: (fields) => canEditSynthetics && !isActionLoading(fields), onClick: (fields) => { updateAlertEnabledState({ @@ -240,4 +243,25 @@ export function useMonitorListColumns({ ], }, ]; + + if (!canEditSynthetics) { + // replace last column with a tooltip + columns[columns.length - 1] = { + align: 'right' as const, + name: i18n.translate('xpack.synthetics.management.monitorList.actions', { + defaultMessage: 'Actions', + }), + render: () => ( + + + + ), + }; + } + + return columns; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx index 2ac8c0f6129d6..55f2d7e80ee1a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { DeleteMonitor } from './delete_monitor'; import { IHttpSerializedFetchError } from '../../../../state/utils/http_error'; import { MonitorListPageState } from '../../../../state'; -import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities'; import { ConfigKey, EncryptedSyntheticsSavedMonitor, @@ -52,7 +51,6 @@ export const MonitorList = ({ }: Props) => { const { euiTheme } = useEuiTheme(); const isXl = useIsWithinMinBreakpoint('xxl'); - const canEditSynthetics = useCanEditSynthetics(); const [monitorPendingDeletion, setMonitorPendingDeletion] = useState(null); @@ -96,7 +94,6 @@ export const MonitorList = ({ }); const columns = useMonitorListColumns({ - canEditSynthetics, loading, overviewStatus, setMonitorPendingDeletion, From 0a38f85002d8f9096bcd35c073f0faebcff9617d Mon Sep 17 00:00:00 2001 From: Antonio Date: Tue, 18 Apr 2023 16:02:11 +0200 Subject: [PATCH 029/100] [Cases] Attaching files to cases (#154436) Fixes #151595 ## Summary In this PR we will be merging a feature branch into `main`. This feature branch is a collection of several different PRs with file functionality for cases. - https://github.com/elastic/kibana/pull/152941 - https://github.com/elastic/kibana/pull/153957 - https://github.com/elastic/kibana/pull/154432 - https://github.com/elastic/kibana/pull/153853 Most of the code was already reviewed so this will mainly be used for testing. - Files tab in the case detail view. - Attach files to a case. - View a list of all files attached to a case (with pagination). - Preview image files attached to a case. - Search for files attached to a case by file name. - Download files attached to a case. - Users are now able to see file activity in the case detail view. - Image files have a different icon and a clickable file name to preview. - Other files have a standard "document" icon and the name is not clickable. - The file can be downloaded by clicking the download icon. ## Release notes Support file attachments in Cases. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- .../file/file_upload/impl/src/file_upload.tsx | 2 +- .../cases/common/api/cases/comment/files.ts | 16 +- .../cases/common/constants/mime_types.ts | 8 +- x-pack/plugins/cases/common/types.ts | 1 + x-pack/plugins/cases/public/application.tsx | 21 +- .../client/attachment_framework/types.ts | 28 +- .../ui/get_all_cases_selector_modal.tsx | 6 +- .../cases/public/client/ui/get_cases.tsx | 6 +- .../public/client/ui/get_cases_context.tsx | 12 +- .../client/ui/get_create_case_flyout.tsx | 6 +- .../public/client/ui/get_recent_cases.tsx | 6 +- .../public/common/mock/test_providers.tsx | 54 +++- .../public/common/use_cases_toast.test.tsx | 20 ++ .../cases/public/common/use_cases_toast.tsx | 3 + .../cases/public/components/app/index.tsx | 10 +- .../components/case_action_bar/actions.tsx | 4 + .../case_view/case_view_page.test.tsx | 3 +- .../components/case_view/case_view_page.tsx | 2 + .../case_view/case_view_tabs.test.tsx | 64 ++++- .../components/case_view/case_view_tabs.tsx | 42 ++- .../components/case_view_files.test.tsx | 114 +++++++++ .../case_view/components/case_view_files.tsx | 104 ++++++++ .../components/case_view/translations.ts | 4 + .../public/components/cases_context/index.tsx | 54 +++- .../public/components/files/add_file.test.tsx | 241 ++++++++++++++++++ .../public/components/files/add_file.tsx | 141 ++++++++++ .../files/file_delete_button.test.tsx | 155 +++++++++++ .../components/files/file_delete_button.tsx | 62 +++++ .../files/file_download_button.test.tsx | 57 +++++ .../components/files/file_download_button.tsx | 46 ++++ .../components/files/file_name_link.test.tsx | 58 +++++ .../components/files/file_name_link.tsx | 44 ++++ .../components/files/file_preview.test.tsx | 38 +++ .../public/components/files/file_preview.tsx | 56 ++++ .../components/files/file_type.test.tsx | 187 ++++++++++++++ .../public/components/files/file_type.tsx | 106 ++++++++ .../components/files/files_table.test.tsx | 233 +++++++++++++++++ .../public/components/files/files_table.tsx | 83 ++++++ .../files/files_utility_bar.test.tsx | 42 +++ .../components/files/files_utility_bar.tsx | 36 +++ .../public/components/files/translations.tsx | 115 +++++++++ .../cases/public/components/files/types.ts | 12 + .../files/use_file_preview.test.tsx | 54 ++++ .../components/files/use_file_preview.tsx | 17 ++ .../files/use_files_table_columns.test.tsx | 73 ++++++ .../files/use_files_table_columns.tsx | 71 ++++++ .../public/components/files/utils.test.tsx | 97 +++++++ .../cases/public/components/files/utils.tsx | 61 +++++ .../lens/use_lens_open_visualization.tsx | 3 + .../components/property_actions/index.tsx | 24 +- .../user_actions/comment/comment.test.tsx | 195 +++++++++++++- .../comment/registered_attachments.tsx | 52 ++-- .../alert_property_actions.tsx | 6 +- .../property_actions.test.tsx | 2 + .../property_actions/property_actions.tsx | 4 +- ...ered_attachments_property_actions.test.tsx | 19 +- ...egistered_attachments_property_actions.tsx | 11 +- .../user_comment_property_actions.tsx | 5 + .../cases/public/containers/__mocks__/api.ts | 10 + .../cases/public/containers/api.test.tsx | 27 ++ x-pack/plugins/cases/public/containers/api.ts | 17 ++ .../cases/public/containers/constants.ts | 4 + .../plugins/cases/public/containers/mock.ts | 15 ++ .../cases/public/containers/translations.ts | 8 + .../use_delete_file_attachment.test.tsx | 120 +++++++++ .../containers/use_delete_file_attachment.tsx | 43 ++++ .../use_get_case_file_stats.test.tsx | 64 +++++ .../containers/use_get_case_file_stats.tsx | 57 +++++ .../containers/use_get_case_files.test.tsx | 70 +++++ .../public/containers/use_get_case_files.tsx | 61 +++++ x-pack/plugins/cases/public/files/index.ts | 3 + .../public/internal_attachments/index.ts | 15 ++ x-pack/plugins/cases/public/plugin.ts | 7 + x-pack/plugins/cases/public/types.ts | 4 +- .../common/limiter_checker/test_utils.ts | 2 +- x-pack/plugins/cases/tsconfig.json | 4 + .../cases_api_integration/common/lib/mock.ts | 2 +- .../public/attachments/external_reference.tsx | 13 +- .../public/attachments/persistable_state.tsx | 11 +- 80 files changed, 3440 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx create mode 100644 x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx create mode 100644 x-pack/plugins/cases/public/components/files/add_file.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/add_file.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_delete_button.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_download_button.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_download_button.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_name_link.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_name_link.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_preview.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_preview.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_type.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/file_type.tsx create mode 100644 x-pack/plugins/cases/public/components/files/files_table.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/files_table.tsx create mode 100644 x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/files_utility_bar.tsx create mode 100644 x-pack/plugins/cases/public/components/files/translations.tsx create mode 100644 x-pack/plugins/cases/public/components/files/types.ts create mode 100644 x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/use_file_preview.tsx create mode 100644 x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx create mode 100644 x-pack/plugins/cases/public/components/files/utils.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/utils.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_file_stats.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_files.tsx create mode 100644 x-pack/plugins/cases/public/internal_attachments/index.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 74d7df9394153..93d5507c53a1e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -7,7 +7,7 @@ pageLoadAssetSize: banners: 17946 bfetch: 22837 canvas: 1066647 - cases: 144442 + cases: 170000 charts: 55000 cloud: 21076 cloudChat: 19894 diff --git a/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx index 498a4a93b5fe4..45e74312e1e55 100644 --- a/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx @@ -19,7 +19,7 @@ import { context } from './context'; /** * An object representing an uploaded file */ -interface UploadedFile { +export interface UploadedFile { /** * The ID that was generated for the uploaded file */ diff --git a/x-pack/plugins/cases/common/api/cases/comment/files.ts b/x-pack/plugins/cases/common/api/cases/comment/files.ts index 66555b1a584d9..af42a7a779e55 100644 --- a/x-pack/plugins/cases/common/api/cases/comment/files.ts +++ b/x-pack/plugins/cases/common/api/cases/comment/files.ts @@ -9,15 +9,15 @@ import * as rt from 'io-ts'; import { MAX_DELETE_FILES } from '../../../constants'; import { limitedArraySchema, NonEmptyString } from '../../../schema'; +export const SingleFileAttachmentMetadataRt = rt.type({ + name: rt.string, + extension: rt.string, + mimeType: rt.string, + created: rt.string, +}); + export const FileAttachmentMetadataRt = rt.type({ - files: rt.array( - rt.type({ - name: rt.string, - extension: rt.string, - mimeType: rt.string, - createdAt: rt.string, - }) - ), + files: rt.array(SingleFileAttachmentMetadataRt), }); export type FileAttachmentMetadata = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants/mime_types.ts b/x-pack/plugins/cases/common/constants/mime_types.ts index 9f1f455513dab..c35e5ef674c81 100644 --- a/x-pack/plugins/cases/common/constants/mime_types.ts +++ b/x-pack/plugins/cases/common/constants/mime_types.ts @@ -8,7 +8,7 @@ /** * These were retrieved from https://www.iana.org/assignments/media-types/media-types.xhtml#image */ -const imageMimeTypes = [ +export const imageMimeTypes = [ 'image/aces', 'image/apng', 'image/avci', @@ -87,9 +87,9 @@ const imageMimeTypes = [ 'image/wmf', ]; -const textMimeTypes = ['text/plain', 'text/csv', 'text/json', 'application/json']; +export const textMimeTypes = ['text/plain', 'text/csv', 'text/json', 'application/json']; -const compressionMimeTypes = [ +export const compressionMimeTypes = [ 'application/zip', 'application/gzip', 'application/x-bzip', @@ -98,7 +98,7 @@ const compressionMimeTypes = [ 'application/x-tar', ]; -const pdfMimeTypes = ['application/pdf']; +export const pdfMimeTypes = ['application/pdf']; export const ALLOWED_MIME_TYPES = [ ...imageMimeTypes, diff --git a/x-pack/plugins/cases/common/types.ts b/x-pack/plugins/cases/common/types.ts index 3ff14b0905110..32d6b34b11c16 100644 --- a/x-pack/plugins/cases/common/types.ts +++ b/x-pack/plugins/cases/common/types.ts @@ -24,4 +24,5 @@ export type SnakeToCamelCase = T extends Record export enum CASE_VIEW_PAGE_TABS { ALERTS = 'alerts', ACTIVITY = 'activity', + FILES = 'files', } diff --git a/x-pack/plugins/cases/public/application.tsx b/x-pack/plugins/cases/public/application.tsx index bac423a9f8292..742f254472160 100644 --- a/x-pack/plugins/cases/public/application.tsx +++ b/x-pack/plugins/cases/public/application.tsx @@ -9,19 +9,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; -import { I18nProvider } from '@kbn/i18n-react'; import { EuiErrorBoundary } from '@elastic/eui'; - +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaContextProvider, KibanaThemeProvider, useUiSetting$, } from '@kbn/kibana-react-plugin/public'; -import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; -import type { RenderAppProps } from './types'; -import { CasesApp } from './components/app'; + +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import type { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; +import type { RenderAppProps } from './types'; + +import { CasesApp } from './components/app'; export const renderApp = (deps: RenderAppProps) => { const { mountParams } = deps; @@ -37,10 +39,15 @@ export const renderApp = (deps: RenderAppProps) => { interface CasesAppWithContextProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + getFilesClient: (scope: string) => ScopedFilesClient; } const CasesAppWithContext: React.FC = React.memo( - ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry }) => { + ({ + externalReferenceAttachmentTypeRegistry, + persistableStateAttachmentTypeRegistry, + getFilesClient, + }) => { const [darkMode] = useUiSetting$('theme:darkMode'); return ( @@ -48,6 +55,7 @@ const CasesAppWithContext: React.FC = React.memo( ); @@ -78,6 +86,7 @@ export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => { deps.externalReferenceAttachmentTypeRegistry } persistableStateAttachmentTypeRegistry={deps.persistableStateAttachmentTypeRegistry} + getFilesClient={pluginsStart.files.filesClientFactory.asScoped} /> diff --git a/x-pack/plugins/cases/public/client/attachment_framework/types.ts b/x-pack/plugins/cases/public/client/attachment_framework/types.ts index 414b8a0086654..95a453b9d0a12 100644 --- a/x-pack/plugins/cases/public/client/attachment_framework/types.ts +++ b/x-pack/plugins/cases/public/client/attachment_framework/types.ts @@ -13,19 +13,38 @@ import type { } from '../../../common/api'; import type { Case } from '../../containers/types'; -export interface AttachmentAction { +export enum AttachmentActionType { + BUTTON = 'button', + CUSTOM = 'custom', +} + +interface BaseAttachmentAction { + type: AttachmentActionType; + label: string; + isPrimary?: boolean; + disabled?: boolean; +} + +interface ButtonAttachmentAction extends BaseAttachmentAction { + type: AttachmentActionType.BUTTON; onClick: () => void; iconType: string; - label: string; color?: EuiButtonProps['color']; - isPrimary?: boolean; } +interface CustomAttachmentAction extends BaseAttachmentAction { + type: AttachmentActionType.CUSTOM; + render: () => JSX.Element; +} + +export type AttachmentAction = ButtonAttachmentAction | CustomAttachmentAction; + export interface AttachmentViewObject { timelineAvatar?: EuiCommentProps['timelineAvatar']; getActions?: (props: Props) => AttachmentAction[]; event?: EuiCommentProps['event']; children?: React.LazyExoticComponent>; + hideDefaultActions?: boolean; } export interface CommonAttachmentViewProps { @@ -46,8 +65,9 @@ export interface AttachmentType { id: string; icon: IconType; displayName: string; - getAttachmentViewObject: () => AttachmentViewObject; + getAttachmentViewObject: (props: Props) => AttachmentViewObject; getAttachmentRemovalObject?: (props: Props) => Pick, 'event'>; + hideDefaultActions?: boolean; } export type ExternalReferenceAttachmentType = AttachmentType; diff --git a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx index b0807b0509135..fc85e84639baa 100644 --- a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetAllCasesSelectorModalPropsInternal = AllCasesSelectorModalProps & CasesContextProps; export type GetAllCasesSelectorModalProps = Omit< GetAllCasesSelectorModalPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; const AllCasesSelectorModalLazy: React.FC = lazy( @@ -23,6 +25,7 @@ const AllCasesSelectorModalLazy: React.FC = lazy( export const getAllCasesSelectorModalLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, hiddenStatuses, @@ -33,6 +36,7 @@ export const getAllCasesSelectorModalLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/client/ui/get_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_cases.tsx index 45c9f30b984d2..36556523fc3a3 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetCasesPropsInternal = CasesProps & CasesContextProps; export type GetCasesProps = Omit< GetCasesPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; const CasesRoutesLazy: React.FC = lazy(() => import('../../components/app/routes')); @@ -22,6 +24,7 @@ const CasesRoutesLazy: React.FC = lazy(() => import('../../component export const getCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, basePath, @@ -39,6 +42,7 @@ export const getCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, basePath, diff --git a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx index 77e6ca3c87e24..9db49ef9776ba 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx @@ -13,7 +13,9 @@ import type { CasesContextProps } from '../../components/cases_context'; export type GetCasesContextPropsInternal = CasesContextProps; export type GetCasesContextProps = Omit< CasesContextProps, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = lazy( @@ -28,6 +30,7 @@ const CasesProviderLazyWrapper = ({ features, children, releasePhase, + getFilesClient, }: GetCasesContextPropsInternal & { children: ReactNode }) => { return ( }> @@ -39,6 +42,7 @@ const CasesProviderLazyWrapper = ({ permissions, features, releasePhase, + getFilesClient, }} > {children} @@ -52,9 +56,12 @@ CasesProviderLazyWrapper.displayName = 'CasesProviderLazyWrapper'; export const getCasesContextLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, }: Pick< GetCasesContextPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >): (() => React.FC) => { const CasesProviderLazyWrapperWithRegistry: React.FC = ({ children, @@ -64,6 +71,7 @@ export const getCasesContextLazy = ({ {...props} externalReferenceAttachmentTypeRegistry={externalReferenceAttachmentTypeRegistry} persistableStateAttachmentTypeRegistry={persistableStateAttachmentTypeRegistry} + getFilesClient={getFilesClient} > {children} diff --git a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx index af932b53e1dde..e52a14033a614 100644 --- a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetCreateCaseFlyoutPropsInternal = CreateCaseFlyoutProps & CasesContextProps; export type GetCreateCaseFlyoutProps = Omit< GetCreateCaseFlyoutPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; export const CreateCaseFlyoutLazy: React.FC = lazy( @@ -23,6 +25,7 @@ export const CreateCaseFlyoutLazy: React.FC = lazy( export const getCreateCaseFlyoutLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, features, @@ -35,6 +38,7 @@ export const getCreateCaseFlyoutLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, features, diff --git a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx index a047c106246da..7c41cc3842bf7 100644 --- a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx @@ -14,7 +14,9 @@ import type { RecentCasesProps } from '../../components/recent_cases'; type GetRecentCasesPropsInternal = RecentCasesProps & CasesContextProps; export type GetRecentCasesProps = Omit< GetRecentCasesPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; const RecentCasesLazy: React.FC = lazy( @@ -23,6 +25,7 @@ const RecentCasesLazy: React.FC = lazy( export const getRecentCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, maxCasesToShow, @@ -31,6 +34,7 @@ export const getRecentCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 2a5a75bf7a789..f0b2e71231bb1 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -9,22 +9,30 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from 'styled-components'; + +import type { RenderOptions, RenderResult } from '@testing-library/react'; +import type { ILicense } from '@kbn/licensing-plugin/public'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; + import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; -import { ThemeProvider } from 'styled-components'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { RenderOptions, RenderResult } from '@testing-library/react'; import { render as reactRender } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import type { ILicense } from '@kbn/licensing-plugin/public'; -import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { FilesContext } from '@kbn/shared-ux-file-context'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { CasesFeatures, CasesPermissions } from '../../../common/ui/types'; -import { CasesProvider } from '../../components/cases_context'; -import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; import type { StartServices } from '../../types'; import type { ReleasePhase } from '../../components/types'; + +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { CasesProvider } from '../../components/cases_context'; +import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; import { allCasesPermissions } from './permissions'; @@ -43,17 +51,35 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul window.scrollTo = jest.fn(); +const mockGetFilesClient = () => { + const mockedFilesClient = createMockFilesClient() as unknown as DeeplyMockedKeys< + ScopedFilesClient + >; + + mockedFilesClient.getFileKind.mockImplementation(() => ({ + id: 'test', + maxSizeBytes: 10000, + http: {}, + })); + + return () => mockedFilesClient; +}; + +export const mockedTestProvidersOwner = [SECURITY_SOLUTION_OWNER]; + /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, features, - owner = [SECURITY_SOLUTION_OWNER], + owner = mockedTestProvidersOwner, permissions = allCasesPermissions(), releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(), license, }) => { + const services = createStartServicesMock({ license }); + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -67,7 +93,7 @@ const TestProvidersComponent: React.FC = ({ }, }); - const services = createStartServicesMock({ license }); + const getFilesClient = mockGetFilesClient(); return ( @@ -82,9 +108,10 @@ const TestProvidersComponent: React.FC = ({ features, owner, permissions, + getFilesClient, }} > - {children} + {children} @@ -104,6 +131,7 @@ export interface AppMockRenderer { coreStart: StartServices; queryClient: QueryClient; AppWrapper: React.FC<{ children: React.ReactElement }>; + getFilesClient: () => ScopedFilesClient; } export const testQueryClient = new QueryClient({ @@ -125,7 +153,7 @@ export const testQueryClient = new QueryClient({ export const createAppMockRenderer = ({ features, - owner = [SECURITY_SOLUTION_OWNER], + owner = mockedTestProvidersOwner, permissions = allCasesPermissions(), releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), @@ -147,6 +175,8 @@ export const createAppMockRenderer = ({ }, }); + const getFilesClient = mockGetFilesClient(); + const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( @@ -161,6 +191,7 @@ export const createAppMockRenderer = ({ owner, permissions, releasePhase, + getFilesClient, }} > {children} @@ -188,6 +219,7 @@ export const createAppMockRenderer = ({ AppWrapper, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, }; }; diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 6c33c86d29d51..d6597e31362e7 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -25,6 +25,7 @@ const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); const errorMock = jest.fn(); + const dangerMock = jest.fn(); const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); const navigateToUrl = jest.fn(); @@ -54,6 +55,7 @@ describe('Use cases toast hook', () => { return { addSuccess: successMock, addError: errorMock, + addDanger: dangerMock, }; }); @@ -352,4 +354,22 @@ describe('Use cases toast hook', () => { }); }); }); + + describe('showDangerToast', () => { + it('should show a danger toast', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showDangerToast('my danger toast'); + + expect(dangerMock).toHaveBeenCalledWith({ + className: 'eui-textBreakWord', + title: 'my danger toast', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index fd143345e2deb..26027905f8f0e 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -169,6 +169,9 @@ export const useCasesToast = () => { showSuccessToast: (title: string) => { toasts.addSuccess({ title, className: 'eui-textBreakWord' }); }, + showDangerToast: (title: string) => { + toasts.addDanger({ title, className: 'eui-textBreakWord' }); + }, showInfoToast: (title: string, text?: string) => { toasts.addInfo({ title, diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index 42ef9b658fea7..f53e7edf9356a 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; -import { APP_OWNER } from '../../../common/constants'; + +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; + import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; + +import { APP_OWNER } from '../../../common/constants'; import { getCasesLazy } from '../../client/ui/get_cases'; import { useApplicationCapabilities } from '../../common/lib/kibana'; - import { Wrapper } from '../wrappers'; import type { CasesRoutesProps } from './types'; @@ -20,11 +23,13 @@ export type CasesProps = CasesRoutesProps; interface CasesAppProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + getFilesClient: (scope: string) => ScopedFilesClient; } const CasesAppComponent: React.FC = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, }) => { const userCapabilities = useApplicationCapabilities(); @@ -33,6 +38,7 @@ const CasesAppComponent: React.FC = ({ {getCasesLazy({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + getFilesClient, owner: [APP_OWNER], useFetchAlertData: () => [false, {}], permissions: userCapabilities.generalCases, diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index 87cd1fc732a30..b80cd5c2dbe74 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -16,6 +16,7 @@ import type { Case } from '../../../common/ui/types'; import { useAllCasesNavigation } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesToast } from '../../common/use_cases_toast'; +import { AttachmentActionType } from '../../client/attachment_framework/types'; interface CaseViewActions { caseData: Case; @@ -40,6 +41,7 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal const propertyActions = useMemo( () => [ { + type: AttachmentActionType.BUTTON as const, iconType: 'copyClipboard', label: i18n.COPY_ID_ACTION_LABEL, onClick: () => { @@ -50,6 +52,7 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) ? [ { + type: AttachmentActionType.BUTTON as const, iconType: 'popout', label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), @@ -59,6 +62,7 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal ...(permissions.delete ? [ { + type: AttachmentActionType.BUTTON as const, iconType: 'trash', label: i18n.DELETE_CASE(), color: 'danger' as const, diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index bf348124e4616..f247945c7c700 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -493,8 +493,9 @@ describe('CaseViewPage', () => { it('renders tabs correctly', async () => { const result = appMockRenderer.render(); await act(async () => { - expect(result.getByTestId('case-view-tab-title-alerts')).toBeTruthy(); expect(result.getByTestId('case-view-tab-title-activity')).toBeTruthy(); + expect(result.getByTestId('case-view-tab-title-alerts')).toBeTruthy(); + expect(result.getByTestId('case-view-tab-title-files')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index a26793e501897..55245de4b22b2 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -18,6 +18,7 @@ import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs'; import { WhitePageWrapperNoBorder } from '../wrappers'; import { CaseViewActivity } from './components/case_view_activity'; import { CaseViewAlerts } from './components/case_view_alerts'; +import { CaseViewFiles } from './components/case_view_files'; import { CaseViewMetrics } from './metrics'; import type { CaseViewPageProps } from './types'; import { useRefreshCaseViewPage } from './use_on_refresh_case_view_page'; @@ -140,6 +141,7 @@ export const CaseViewPage = React.memo( {activeTabId === CASE_VIEW_PAGE_TABS.ALERTS && features.alerts.enabled && ( )} + {activeTabId === CASE_VIEW_PAGE_TABS.FILES && }
{timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null} diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx index a3da7d90267cf..bd532d95ba58b 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx @@ -8,23 +8,28 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; + import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; +import type { UseGetCase } from '../../containers/use_get_case'; +import type { CaseViewTabsProps } from './case_view_tabs'; + +import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; import '../../common/mock/match_media'; +import { createAppMockRenderer } from '../../common/mock'; import { useCaseViewNavigation } from '../../common/navigation/hooks'; -import type { UseGetCase } from '../../containers/use_get_case'; import { useGetCase } from '../../containers/use_get_case'; import { CaseViewTabs } from './case_view_tabs'; import { caseData, defaultGetCase } from './mocks'; -import type { CaseViewTabsProps } from './case_view_tabs'; -import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; +import { useGetCaseFileStats } from '../../containers/use_get_case_file_stats'; jest.mock('../../containers/use_get_case'); jest.mock('../../common/navigation/hooks'); jest.mock('../../common/hooks'); +jest.mock('../../containers/use_get_case_file_stats'); const useFetchCaseMock = useGetCase as jest.Mock; const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock; +const useGetCaseFileStatsMock = useGetCaseFileStats as jest.Mock; const mockGetCase = (props: Partial = {}) => { const data = { @@ -45,8 +50,10 @@ export const caseProps: CaseViewTabsProps = { describe('CaseViewTabs', () => { let appMockRenderer: AppMockRenderer; + const data = { total: 3 }; beforeEach(() => { + useGetCaseFileStatsMock.mockReturnValue({ data }); mockGetCase(); appMockRenderer = createAppMockRenderer(); @@ -62,6 +69,7 @@ describe('CaseViewTabs', () => { expect(await screen.findByTestId('case-view-tab-title-activity')).toBeInTheDocument(); expect(await screen.findByTestId('case-view-tab-title-alerts')).toBeInTheDocument(); + expect(await screen.findByTestId('case-view-tab-title-files')).toBeInTheDocument(); }); it('renders the activity tab by default', async () => { @@ -82,6 +90,40 @@ describe('CaseViewTabs', () => { ); }); + it('shows the files tab as active', async () => { + appMockRenderer.render(); + + expect(await screen.findByTestId('case-view-tab-title-files')).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it('shows the files tab with the correct count and colour', async () => { + appMockRenderer.render(); + + const badge = await screen.findByTestId('case-view-files-stats-badge'); + + expect(badge.getAttribute('class')).toMatch(/accent/); + expect(badge).toHaveTextContent('3'); + }); + + it('do not show count on the files tab if the call isLoading', async () => { + useGetCaseFileStatsMock.mockReturnValue({ isLoading: true, data }); + + appMockRenderer.render(); + + expect(screen.queryByTestId('case-view-files-stats-badge')).not.toBeInTheDocument(); + }); + + it('the files tab count has a different colour if the tab is not active', async () => { + appMockRenderer.render(); + + expect( + (await screen.findByTestId('case-view-files-stats-badge')).getAttribute('class') + ).not.toMatch(/accent/); + }); + it('navigates to the activity tab when the activity tab is clicked', async () => { const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; appMockRenderer.render(); @@ -109,4 +151,18 @@ describe('CaseViewTabs', () => { }); }); }); + + it('navigates to the files tab when the files tab is clicked', async () => { + const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; + appMockRenderer.render(); + + userEvent.click(await screen.findByTestId('case-view-tab-title-files')); + + await waitFor(() => { + expect(navigateToCaseViewMock).toHaveBeenCalledWith({ + detailName: caseData.id, + tabId: CASE_VIEW_PAGE_TABS.FILES, + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx index 746311051f147..630248bf79d52 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx @@ -5,20 +5,49 @@ * 2.0. */ -import { EuiBetaBadge, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; +import { EuiBetaBadge, EuiNotificationBadge, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; import { useCaseViewNavigation } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; import { EXPERIMENTAL_DESC, EXPERIMENTAL_LABEL } from '../header_page/translations'; -import { ACTIVITY_TAB, ALERTS_TAB } from './translations'; +import { ACTIVITY_TAB, ALERTS_TAB, FILES_TAB } from './translations'; import type { Case } from '../../../common'; +import { useGetCaseFileStats } from '../../containers/use_get_case_file_stats'; const ExperimentalBadge = styled(EuiBetaBadge)` margin-left: 5px; `; +const StyledNotificationBadge = styled(EuiNotificationBadge)` + margin-left: 5px; +`; + +const FilesTab = ({ + activeTab, + fileStatsData, + isLoading, +}: { + activeTab: string; + fileStatsData: { total: number } | undefined; + isLoading: boolean; +}) => ( + <> + {FILES_TAB} + {!isLoading && fileStatsData && ( + + {fileStatsData.total > 0 ? fileStatsData.total : 0} + + )} + +); + +FilesTab.displayName = 'FilesTab'; + export interface CaseViewTabsProps { caseData: Case; activeTab: CASE_VIEW_PAGE_TABS; @@ -27,6 +56,7 @@ export interface CaseViewTabsProps { export const CaseViewTabs = React.memo(({ caseData, activeTab }) => { const { features } = useCasesContext(); const { navigateToCaseView } = useCaseViewNavigation(); + const { data: fileStatsData, isLoading } = useGetCaseFileStats({ caseId: caseData.id }); const tabs = useMemo( () => [ @@ -56,8 +86,14 @@ export const CaseViewTabs = React.memo(({ caseData, activeTab }, ] : []), + { + id: CASE_VIEW_PAGE_TABS.FILES, + name: ( + + ), + }, ], - [features.alerts.enabled, features.alerts.isExperimental] + [activeTab, features.alerts.enabled, features.alerts.isExperimental, fileStatsData, isLoading] ); const renderTabs = useCallback(() => { diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx new file mode 100644 index 0000000000000..dc5b937bd8781 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { Case } from '../../../../common'; +import type { AppMockRenderer } from '../../../common/mock'; + +import { createAppMockRenderer } from '../../../common/mock'; +import { alertCommentWithIndices, basicCase } from '../../../containers/mock'; +import { useGetCaseFiles } from '../../../containers/use_get_case_files'; +import { CaseViewFiles, DEFAULT_CASE_FILES_FILTERING_OPTIONS } from './case_view_files'; + +jest.mock('../../../containers/use_get_case_files'); + +const useGetCaseFilesMock = useGetCaseFiles as jest.Mock; + +const caseData: Case = { + ...basicCase, + comments: [...basicCase.comments, alertCommentWithIndices], +}; + +describe('Case View Page files tab', () => { + let appMockRender: AppMockRenderer; + + useGetCaseFilesMock.mockReturnValue({ + data: { files: [], total: 11 }, + isLoading: false, + }); + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the utility bar for the files table', async () => { + appMockRender.render(); + + expect((await screen.findAllByTestId('cases-files-add')).length).toBe(2); + expect(await screen.findByTestId('cases-files-search')).toBeInTheDocument(); + }); + + it('should render the files table', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + }); + + it('clicking table pagination triggers calls to useGetCaseFiles', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('pagination-button-next')); + + await waitFor(() => + expect(useGetCaseFilesMock).toHaveBeenCalledWith({ + caseId: basicCase.id, + page: DEFAULT_CASE_FILES_FILTERING_OPTIONS.page + 1, + perPage: DEFAULT_CASE_FILES_FILTERING_OPTIONS.perPage, + }) + ); + }); + + it('changing perPage value triggers calls to useGetCaseFiles', async () => { + const targetPagination = 50; + + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('tablePaginationPopoverButton')); + + const pageSizeOption = screen.getByTestId('tablePagination-50-rows'); + + pageSizeOption.style.pointerEvents = 'all'; + + userEvent.click(pageSizeOption); + + await waitFor(() => + expect(useGetCaseFilesMock).toHaveBeenCalledWith({ + caseId: basicCase.id, + page: DEFAULT_CASE_FILES_FILTERING_OPTIONS.page, + perPage: targetPagination, + }) + ); + }); + + it('search by word triggers calls to useGetCaseFiles', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + + await userEvent.type(screen.getByTestId('cases-files-search'), 'Foobar{enter}'); + + await waitFor(() => + expect(useGetCaseFilesMock).toHaveBeenCalledWith({ + caseId: basicCase.id, + page: DEFAULT_CASE_FILES_FILTERING_OPTIONS.page, + perPage: DEFAULT_CASE_FILES_FILTERING_OPTIONS.perPage, + searchTerm: 'Foobar', + }) + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx new file mode 100644 index 0000000000000..54693acfa2390 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { isEqual } from 'lodash/fp'; +import React, { useCallback, useMemo, useState } from 'react'; + +import type { Criteria } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import type { Case } from '../../../../common/ui/types'; +import type { CaseFilesFilteringOptions } from '../../../containers/use_get_case_files'; + +import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; +import { useGetCaseFiles } from '../../../containers/use_get_case_files'; +import { FilesTable } from '../../files/files_table'; +import { CaseViewTabs } from '../case_view_tabs'; +import { FilesUtilityBar } from '../../files/files_utility_bar'; + +interface CaseViewFilesProps { + caseData: Case; +} + +export const DEFAULT_CASE_FILES_FILTERING_OPTIONS = { + page: 0, + perPage: 10, +}; + +export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { + const [filteringOptions, setFilteringOptions] = useState( + DEFAULT_CASE_FILES_FILTERING_OPTIONS + ); + const { + data: caseFiles, + isLoading, + isPreviousData, + } = useGetCaseFiles({ + ...filteringOptions, + caseId: caseData.id, + }); + + const onTableChange = useCallback( + ({ page }: Criteria) => { + if (page && !isPreviousData) { + setFilteringOptions({ + ...filteringOptions, + page: page.index, + perPage: page.size, + }); + } + }, + [filteringOptions, isPreviousData] + ); + + const onSearchChange = useCallback( + (newSearch) => { + const trimSearch = newSearch.trim(); + if (!isEqual(trimSearch, filteringOptions.searchTerm)) { + setFilteringOptions({ + ...filteringOptions, + searchTerm: trimSearch, + }); + } + }, + [filteringOptions] + ); + + const pagination = useMemo( + () => ({ + pageIndex: filteringOptions.page, + pageSize: filteringOptions.perPage, + totalItemCount: caseFiles?.total ?? 0, + pageSizeOptions: [10, 25, 50], + showPerPageOptions: true, + }), + [filteringOptions.page, filteringOptions.perPage, caseFiles?.total] + ); + + return ( + + + + + + + + + + + + ); +}; + +CaseViewFiles.displayName = 'CaseViewFiles'; diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts index d71c56fc97fca..8fc80c1a0aba3 100644 --- a/x-pack/plugins/cases/public/components/case_view/translations.ts +++ b/x-pack/plugins/cases/public/components/case_view/translations.ts @@ -165,6 +165,10 @@ export const ALERTS_TAB = i18n.translate('xpack.cases.caseView.tabs.alerts', { defaultMessage: 'Alerts', }); +export const FILES_TAB = i18n.translate('xpack.cases.caseView.tabs.files', { + defaultMessage: 'Files', +}); + export const ALERTS_EMPTY_DESCRIPTION = i18n.translate( 'xpack.cases.caseView.tabs.alerts.emptyDescription', { diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index 4e31fffdd7701..dc7eac6381b4d 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -5,25 +5,34 @@ * 2.0. */ -import type { Dispatch } from 'react'; -import React, { useState, useEffect, useReducer } from 'react'; +import type { Dispatch, ReactNode } from 'react'; + import { merge } from 'lodash'; +import React, { useCallback, useEffect, useState, useReducer } from 'react'; import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; -import { DEFAULT_FEATURES } from '../../../common/constants'; -import { DEFAULT_BASE_PATH } from '../../common/navigation'; -import { useApplication } from './use_application'; + +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; + +import { FilesContext } from '@kbn/shared-ux-file-context'; + import type { CasesContextStoreAction } from './cases_context_reducer'; -import { casesContextReducer, getInitialCasesContextState } from './cases_context_reducer'; import type { CasesFeaturesAllRequired, CasesFeatures, CasesPermissions, } from '../../containers/types'; -import { CasesGlobalComponents } from './cases_global_components'; import type { ReleasePhase } from '../types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import { CasesGlobalComponents } from './cases_global_components'; +import { DEFAULT_FEATURES } from '../../../common/constants'; +import { constructFileKindIdByOwner } from '../../../common/files'; +import { DEFAULT_BASE_PATH } from '../../common/navigation'; +import { useApplication } from './use_application'; +import { casesContextReducer, getInitialCasesContextState } from './cases_context_reducer'; +import { isRegisteredOwner } from '../../files'; + export type CasesContextValueDispatch = Dispatch; export interface CasesContextValue { @@ -50,6 +59,7 @@ export interface CasesContextProps basePath?: string; features?: CasesFeatures; releasePhase?: ReleasePhase; + getFilesClient: (scope: string) => ScopedFilesClient; } export const CasesContext = React.createContext(undefined); @@ -69,6 +79,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ basePath = DEFAULT_BASE_PATH, features = {}, releasePhase = 'ga', + getFilesClient, }, }) => { const { appId, appTitle } = useApplication(); @@ -114,10 +125,35 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ } }, [appTitle, appId]); + const applyFilesContext = useCallback( + (contextChildren: ReactNode) => { + if (owner.length === 0) { + return contextChildren; + } + + if (isRegisteredOwner(owner[0])) { + return ( + + {contextChildren} + + ); + } else { + throw new Error( + 'Invalid owner provided to cases context. See https://github.com/elastic/kibana/blob/main/x-pack/plugins/cases/README.md#casescontext-setup' + ); + } + }, + [getFilesClient, owner] + ); + return isCasesContextValue(value) ? ( - - {children} + {applyFilesContext( + <> + + {children} + + )} ) : null; }; diff --git a/x-pack/plugins/cases/public/components/files/add_file.test.tsx b/x-pack/plugins/cases/public/components/files/add_file.test.tsx new file mode 100644 index 0000000000000..911f8a4df538d --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/add_file.test.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { FileUploadProps } from '@kbn/shared-ux-file-upload'; + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; + +import * as api from '../../containers/api'; +import { + buildCasesPermissions, + createAppMockRenderer, + mockedTestProvidersOwner, +} from '../../common/mock'; +import { AddFile } from './add_file'; +import { useToasts } from '../../common/lib/kibana'; + +import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { basicCaseId, basicFileMock } from '../../containers/mock'; + +jest.mock('../../containers/api'); +jest.mock('../../containers/use_create_attachments'); +jest.mock('../../common/lib/kibana'); + +const useToastsMock = useToasts as jest.Mock; +const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; + +const mockedExternalReferenceId = 'externalReferenceId'; +const validateMetadata = jest.fn(); +const mockFileUpload = jest + .fn() + .mockImplementation( + ({ + kind, + onDone, + onError, + meta, + }: Required>) => ( + <> + + + + + ) + ); + +jest.mock('@kbn/shared-ux-file-upload', () => { + const original = jest.requireActual('@kbn/shared-ux-file-upload'); + return { + ...original, + FileUpload: (props: unknown) => mockFileUpload(props), + }; +}); + +describe('AddFile', () => { + let appMockRender: AppMockRenderer; + + const successMock = jest.fn(); + const errorMock = jest.fn(); + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + addError: errorMock, + }; + }); + + const createAttachmentsMock = jest.fn(); + + useCreateAttachmentsMock.mockReturnValue({ + isLoading: false, + createAttachments: createAttachmentsMock, + }); + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); + }); + + it('AddFile is not rendered if user has no create permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ create: false }), + }); + + appMockRender.render(); + + expect(screen.queryByTestId('cases-files-add')).not.toBeInTheDocument(); + }); + + it('AddFile is not rendered if user has no update permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ update: false }), + }); + + appMockRender.render(); + + expect(screen.queryByTestId('cases-files-add')).not.toBeInTheDocument(); + }); + + it('clicking button renders modal', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + }); + + it('createAttachments called with right parameters', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnDone')); + + await waitFor(() => + expect(createAttachmentsMock).toBeCalledWith({ + caseId: 'foobar', + caseOwner: mockedTestProvidersOwner[0], + data: [ + { + externalReferenceAttachmentTypeId: '.files', + externalReferenceId: mockedExternalReferenceId, + externalReferenceMetadata: { + files: [ + { + created: '2020-02-19T23:06:33.798Z', + extension: 'png', + mimeType: 'image/png', + name: 'my-super-cool-screenshot', + }, + ], + }, + externalReferenceStorage: { soType: 'file', type: 'savedObject' }, + type: 'externalReference', + }, + ], + throwOnError: true, + updateCase: expect.any(Function), + }) + ); + + await waitFor(() => + expect(successMock).toHaveBeenCalledWith({ + className: 'eui-textBreakWord', + title: `File ${basicFileMock.name} uploaded successfully`, + }) + ); + }); + + it('failed upload displays error toast', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnError')); + + expect(errorMock).toHaveBeenCalledWith( + { name: 'upload error name', message: 'upload error message' }, + { + title: 'Failed to upload file', + } + ); + }); + + it('correct metadata is passed to FileUpload component', async () => { + const caseId = 'foobar'; + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testMetadata')); + + await waitFor(() => + expect(validateMetadata).toHaveBeenCalledWith({ + caseIds: [caseId], + owner: [mockedTestProvidersOwner[0]], + }) + ); + }); + + it('deleteFileAttachments is called correctly if createAttachments fails', async () => { + const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); + + createAttachmentsMock.mockImplementation(() => { + throw new Error(); + }); + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnDone')); + + expect(spyOnDeleteFileAttachments).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileIds: [mockedExternalReferenceId], + signal: expect.any(AbortSignal), + }); + + createAttachmentsMock.mockRestore(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/add_file.tsx b/x-pack/plugins/cases/public/components/files/add_file.tsx new file mode 100644 index 0000000000000..a3c9fba1188ea --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButton, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; + +import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; + +import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; +import { FileUpload } from '@kbn/shared-ux-file-upload'; + +import { constructFileKindIdByOwner } from '../../../common/files'; +import type { Owner } from '../../../common/constants/types'; + +import { CommentType, ExternalReferenceStorageType } from '../../../common'; +import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; +import { useCasesToast } from '../../common/use_cases_toast'; +import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import * as i18n from './translations'; +import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; +import { deleteFileAttachments } from '../../containers/api'; + +interface AddFileProps { + caseId: string; +} + +const AddFileComponent: React.FC = ({ caseId }) => { + const { owner, permissions } = useCasesContext(); + const { showDangerToast, showErrorToast, showSuccessToast } = useCasesToast(); + const { isLoading, createAttachments } = useCreateAttachments(); + const refreshAttachmentsTable = useRefreshCaseViewPage(); + const [isModalVisible, setIsModalVisible] = useState(false); + + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + + const onError = useCallback( + (error) => { + showErrorToast(error, { + title: i18n.FAILED_UPLOAD, + }); + }, + [showErrorToast] + ); + + const onUploadDone = useCallback( + async (chosenFiles: UploadedFile[]) => { + if (chosenFiles.length === 0) { + showDangerToast(i18n.FAILED_UPLOAD); + return; + } + + const file = chosenFiles[0]; + + try { + await createAttachments({ + caseId, + caseOwner: owner[0], + data: [ + { + type: CommentType.externalReference, + externalReferenceId: file.id, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_SO_TYPE, + }, + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + files: [ + { + name: file.fileJSON.name, + extension: file.fileJSON.extension ?? '', + mimeType: file.fileJSON.mimeType ?? '', + created: file.fileJSON.created, + }, + ], + }, + }, + ], + updateCase: refreshAttachmentsTable, + throwOnError: true, + }); + + showSuccessToast(i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name)); + } catch (error) { + // error toast is handled inside createAttachments + + // we need to delete the file if attachment creation failed + const abortCtrlRef = new AbortController(); + return deleteFileAttachments({ caseId, fileIds: [file.id], signal: abortCtrlRef.signal }); + } + + closeModal(); + }, + [caseId, createAttachments, owner, refreshAttachmentsTable, showDangerToast, showSuccessToast] + ); + + return permissions.create && permissions.update ? ( + + + {i18n.ADD_FILE} + + {isModalVisible && ( + + + {i18n.ADD_FILE} + + + + + + )} + + ) : null; +}; +AddFileComponent.displayName = 'AddFile'; + +export const AddFile = React.memo(AddFileComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx new file mode 100644 index 0000000000000..38ed8a20eab40 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; + +import { buildCasesPermissions, createAppMockRenderer } from '../../common/mock'; +import { basicCaseId, basicFileMock } from '../../containers/mock'; +import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; +import { FileDeleteButton } from './file_delete_button'; + +jest.mock('../../containers/use_delete_file_attachment'); + +const useDeleteFileAttachmentMock = useDeleteFileAttachment as jest.Mock; + +describe('FileDeleteButton', () => { + let appMockRender: AppMockRenderer; + const mutate = jest.fn(); + + useDeleteFileAttachmentMock.mockReturnValue({ isLoading: false, mutate }); + + describe('isIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders delete button correctly', async () => { + appMockRender.render( + + ); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + + expect(useDeleteFileAttachmentMock).toBeCalledTimes(1); + }); + + it('clicking delete button opens the confirmation modal', async () => { + appMockRender.render( + + ); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + it('clicking delete button in the confirmation modal calls deleteFileAttachment with proper params', async () => { + appMockRender.render( + + ); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('confirmModalConfirmButton')); + + await waitFor(() => { + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileId: basicFileMock.id, + }); + }); + }); + + it('delete button is not rendered if user has no delete permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ delete: false }), + }); + + appMockRender.render( + + ); + + expect(screen.queryByTestId('cases-files-delete-button')).not.toBeInTheDocument(); + }); + }); + + describe('not isIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders delete button correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + + expect(useDeleteFileAttachmentMock).toBeCalledTimes(1); + }); + + it('clicking delete button opens the confirmation modal', async () => { + appMockRender.render(); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + it('clicking delete button in the confirmation modal calls deleteFileAttachment with proper params', async () => { + appMockRender.render(); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('confirmModalConfirmButton')); + + await waitFor(() => { + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileId: basicFileMock.id, + }); + }); + }); + + it('delete button is not rendered if user has no delete permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ delete: false }), + }); + + appMockRender.render(); + + expect(screen.queryByTestId('cases-files-delete-button')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button.tsx new file mode 100644 index 0000000000000..f344b942aa2c2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_delete_button.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import * as i18n from './translations'; +import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; +import { useDeletePropertyAction } from '../user_actions/property_actions/use_delete_property_action'; +import { DeleteAttachmentConfirmationModal } from '../user_actions/delete_attachment_confirmation_modal'; +import { useCasesContext } from '../cases_context/use_cases_context'; + +interface FileDeleteButtonProps { + caseId: string; + fileId: string; + isIcon?: boolean; +} + +const FileDeleteButtonComponent: React.FC = ({ caseId, fileId, isIcon }) => { + const { permissions } = useCasesContext(); + const { isLoading, mutate: deleteFileAttachment } = useDeleteFileAttachment(); + + const { showDeletionModal, onModalOpen, onConfirm, onCancel } = useDeletePropertyAction({ + onDelete: () => deleteFileAttachment({ caseId, fileId }), + }); + + const buttonProps = { + iconType: 'trash', + 'aria-label': i18n.DELETE_FILE, + color: 'danger' as const, + isDisabled: isLoading, + onClick: onModalOpen, + 'data-test-subj': 'cases-files-delete-button', + }; + + return permissions.delete ? ( + <> + {isIcon ? ( + + ) : ( + {i18n.DELETE_FILE} + )} + {showDeletionModal ? ( + + ) : null} + + ) : ( + <> + ); +}; +FileDeleteButtonComponent.displayName = 'FileDeleteButton'; + +export const FileDeleteButton = React.memo(FileDeleteButtonComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_download_button.test.tsx b/x-pack/plugins/cases/public/components/files/file_download_button.test.tsx new file mode 100644 index 0000000000000..0c729900a9ea6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_download_button.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock'; +import { FileDownloadButton } from './file_download_button'; +import { basicFileMock } from '../../containers/mock'; +import { constructFileKindIdByOwner } from '../../../common/files'; + +describe('FileDownloadButton', () => { + let appMockRender: AppMockRenderer; + + describe('isIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders download button with correct href', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + + expect(appMockRender.getFilesClient().getDownloadHref).toBeCalledTimes(1); + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + }); + }); + + describe('not isIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders download button with correct href', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + + expect(appMockRender.getFilesClient().getDownloadHref).toBeCalledTimes(1); + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_download_button.tsx b/x-pack/plugins/cases/public/components/files/file_download_button.tsx new file mode 100644 index 0000000000000..856c7000ba9d5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_download_button.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; + +import type { Owner } from '../../../common/constants/types'; + +import { constructFileKindIdByOwner } from '../../../common/files'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import * as i18n from './translations'; + +interface FileDownloadButtonProps { + fileId: string; + isIcon?: boolean; +} + +const FileDownloadButtonComponent: React.FC = ({ fileId, isIcon }) => { + const { owner } = useCasesContext(); + const { client: filesClient } = useFilesContext(); + + const buttonProps = { + iconType: 'download', + 'aria-label': i18n.DOWNLOAD_FILE, + href: filesClient.getDownloadHref({ + fileKind: constructFileKindIdByOwner(owner[0] as Owner), + id: fileId, + }), + 'data-test-subj': 'cases-files-download-button', + }; + + return isIcon ? ( + + ) : ( + {i18n.DOWNLOAD_FILE} + ); +}; +FileDownloadButtonComponent.displayName = 'FileDownloadButton'; + +export const FileDownloadButton = React.memo(FileDownloadButtonComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx new file mode 100644 index 0000000000000..39c62322dedeb --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { FileNameLink } from './file_name_link'; +import { basicFileMock } from '../../containers/mock'; + +describe('FileNameLink', () => { + let appMockRender: AppMockRenderer; + + const defaultProps = { + file: basicFileMock, + showPreview: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders clickable name if file is image', async () => { + appMockRender.render(); + + const nameLink = await screen.findByTestId('cases-files-name-link'); + + expect(nameLink).toBeInTheDocument(); + + userEvent.click(nameLink); + + expect(defaultProps.showPreview).toHaveBeenCalled(); + }); + + it('renders simple text name if file is not image', async () => { + appMockRender.render( + + ); + + const nameLink = await screen.findByTestId('cases-files-name-text'); + + expect(nameLink).toBeInTheDocument(); + + userEvent.click(nameLink); + + expect(defaultProps.showPreview).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_name_link.tsx b/x-pack/plugins/cases/public/components/files/file_name_link.tsx new file mode 100644 index 0000000000000..4c9aedc3ad85b --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_name_link.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiLink } from '@elastic/eui'; + +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import * as i18n from './translations'; +import { isImage } from './utils'; + +interface FileNameLinkProps { + file: Pick; + showPreview: () => void; +} + +const FileNameLinkComponent: React.FC = ({ file, showPreview }) => { + let fileName = file.name; + + if (typeof file.extension !== 'undefined') { + fileName += `.${file.extension}`; + } + + if (isImage(file)) { + return ( + + {fileName} + + ); + } else { + return ( + + {fileName} + + ); + } +}; +FileNameLinkComponent.displayName = 'FileNameLink'; + +export const FileNameLink = React.memo(FileNameLinkComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx new file mode 100644 index 0000000000000..b02df3a82228f --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { screen, waitFor } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/files'; +import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock'; +import { basicFileMock } from '../../containers/mock'; +import { FilePreview } from './file_preview'; + +describe('FilePreview', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('FilePreview rendered correctly', async () => { + appMockRender.render(); + + await waitFor(() => + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx new file mode 100644 index 0000000000000..1bb91c5b53ff7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import styled from 'styled-components'; + +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; + +import type { Owner } from '../../../common/constants/types'; + +import { constructFileKindIdByOwner } from '../../../common/files'; +import { useCasesContext } from '../cases_context/use_cases_context'; + +interface FilePreviewProps { + closePreview: () => void; + selectedFile: Pick; +} + +const StyledOverlayMask = styled(EuiOverlayMask)` + padding-block-end: 0vh !important; + + img { + max-height: 85vh; + max-width: 85vw; + object-fit: contain; + } +`; + +export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => { + const { client: filesClient } = useFilesContext(); + const { owner } = useCasesContext(); + + return ( + + + + + + ); +}; + +FilePreview.displayName = 'FilePreview'; diff --git a/x-pack/plugins/cases/public/components/files/file_type.test.tsx b/x-pack/plugins/cases/public/components/files/file_type.test.tsx new file mode 100644 index 0000000000000..8d4fd4c0eabde --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_type.test.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { JsonValue } from '@kbn/utility-types'; + +import { screen } from '@testing-library/react'; + +import type { ExternalReferenceAttachmentViewProps } from '../../client/attachment_framework/types'; +import type { AppMockRenderer } from '../../common/mock'; + +import { AttachmentActionType } from '../../client/attachment_framework/types'; +import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; +import { createAppMockRenderer } from '../../common/mock'; +import { basicCase, basicFileMock } from '../../containers/mock'; +import { getFileType } from './file_type'; +import userEvent from '@testing-library/user-event'; + +describe('getFileType', () => { + const fileType = getFileType(); + + it('invalid props return blank FileAttachmentViewObject', () => { + expect(fileType).toStrictEqual({ + id: FILE_ATTACHMENT_TYPE, + icon: 'document', + displayName: 'File Attachment Type', + getAttachmentViewObject: expect.any(Function), + }); + }); + + describe('getFileAttachmentViewObject', () => { + let appMockRender: AppMockRenderer; + + const attachmentViewProps = { + externalReferenceId: basicFileMock.id, + externalReferenceMetadata: { files: [basicFileMock] }, + caseData: { title: basicCase.title, id: basicCase.id }, + } as unknown as ExternalReferenceAttachmentViewProps; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('event renders a clickable name if the file is an image', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).event); + + expect(await screen.findByText('my-super-cool-screenshot.png')).toBeInTheDocument(); + expect(screen.queryByTestId('cases-files-image-preview')).not.toBeInTheDocument(); + }); + + it('clicking the name rendered in event opens the file preview', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).event); + + userEvent.click(await screen.findByText('my-super-cool-screenshot.png')); + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); + + it('getActions renders a download button', async () => { + appMockRender = createAppMockRenderer(); + + const attachmentViewObject = fileType.getAttachmentViewObject({ ...attachmentViewProps }); + + expect(attachmentViewObject).not.toBeUndefined(); + + // @ts-ignore + const actions = attachmentViewObject.getActions(); + + expect(actions.length).toBe(2); + expect(actions[0]).toStrictEqual({ + type: AttachmentActionType.CUSTOM, + isPrimary: false, + label: 'Download File', + render: expect.any(Function), + }); + + // @ts-ignore + appMockRender.render(actions[0].render()); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + }); + + it('getActions renders a delete button', async () => { + appMockRender = createAppMockRenderer(); + + const attachmentViewObject = fileType.getAttachmentViewObject({ ...attachmentViewProps }); + + expect(attachmentViewObject).not.toBeUndefined(); + + // @ts-ignore + const actions = attachmentViewObject.getActions(); + + expect(actions.length).toBe(2); + expect(actions[1]).toStrictEqual({ + type: AttachmentActionType.CUSTOM, + isPrimary: false, + label: 'Delete File', + render: expect.any(Function), + }); + + // @ts-ignore + appMockRender.render(actions[1].render()); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + }); + + it('clicking the delete button in actions opens deletion modal', async () => { + appMockRender = createAppMockRenderer(); + + const attachmentViewObject = fileType.getAttachmentViewObject({ ...attachmentViewProps }); + + expect(attachmentViewObject).not.toBeUndefined(); + + // @ts-ignore + const actions = attachmentViewObject.getActions(); + + expect(actions.length).toBe(2); + expect(actions[1]).toStrictEqual({ + type: AttachmentActionType.CUSTOM, + isPrimary: false, + label: 'Delete File', + render: expect.any(Function), + }); + + // @ts-ignore + appMockRender.render(actions[1].render()); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + it('empty externalReferenceMetadata returns blank FileAttachmentViewObject', () => { + expect( + fileType.getAttachmentViewObject({ ...attachmentViewProps, externalReferenceMetadata: {} }) + ).toEqual({ + event: 'added an unknown file', + hideDefaultActions: true, + timelineAvatar: 'document', + type: 'regular', + getActions: expect.any(Function), + }); + }); + + it('timelineAvatar is image if file is an image', () => { + expect(fileType.getAttachmentViewObject(attachmentViewProps)).toEqual( + expect.objectContaining({ + timelineAvatar: 'image', + }) + ); + }); + + it('timelineAvatar is document if file is not an image', () => { + expect( + fileType.getAttachmentViewObject({ + ...attachmentViewProps, + externalReferenceMetadata: { + files: [{ ...basicFileMock, mimeType: 'text/csv' } as JsonValue], + }, + }) + ).toEqual( + expect.objectContaining({ + timelineAvatar: 'document', + }) + ); + }); + + it('default actions should be hidden', () => { + expect(fileType.getAttachmentViewObject(attachmentViewProps)).toEqual( + expect.objectContaining({ + hideDefaultActions: true, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_type.tsx b/x-pack/plugins/cases/public/components/files/file_type.tsx new file mode 100644 index 0000000000000..271bf3008e70e --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_type.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import type { + ExternalReferenceAttachmentType, + ExternalReferenceAttachmentViewProps, +} from '../../client/attachment_framework/types'; +import type { DownloadableFile } from './types'; + +import { AttachmentActionType } from '../../client/attachment_framework/types'; +import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; +import { FileDownloadButton } from './file_download_button'; +import { FileNameLink } from './file_name_link'; +import { FilePreview } from './file_preview'; +import * as i18n from './translations'; +import { isImage, isValidFileExternalReferenceMetadata } from './utils'; +import { useFilePreview } from './use_file_preview'; +import { FileDeleteButton } from './file_delete_button'; + +interface FileAttachmentEventProps { + file: DownloadableFile; +} + +const FileAttachmentEvent = ({ file }: FileAttachmentEventProps) => { + const { isPreviewVisible, showPreview, closePreview } = useFilePreview(); + + return ( + <> + {i18n.ADDED} + + {isPreviewVisible && } + + ); +}; + +FileAttachmentEvent.displayName = 'FileAttachmentEvent'; + +function getFileDownloadButton(fileId: string) { + return ; +} + +function getFileDeleteButton(caseId: string, fileId: string) { + return ; +} + +const getFileAttachmentActions = ({ caseId, fileId }: { caseId: string; fileId: string }) => [ + { + type: AttachmentActionType.CUSTOM as const, + render: () => getFileDownloadButton(fileId), + label: i18n.DOWNLOAD_FILE, + isPrimary: false, + }, + { + type: AttachmentActionType.CUSTOM as const, + render: () => getFileDeleteButton(caseId, fileId), + label: i18n.DELETE_FILE, + isPrimary: false, + }, +]; + +const getFileAttachmentViewObject = (props: ExternalReferenceAttachmentViewProps) => { + const caseId = props.caseData.id; + const fileId = props.externalReferenceId; + + if (!isValidFileExternalReferenceMetadata(props.externalReferenceMetadata)) { + return { + type: 'regular', + event: i18n.ADDED_UNKNOWN_FILE, + timelineAvatar: 'document', + getActions: () => [ + { + type: AttachmentActionType.CUSTOM as const, + render: () => getFileDeleteButton(caseId, fileId), + label: i18n.DELETE_FILE, + isPrimary: false, + }, + ], + hideDefaultActions: true, + }; + } + + const fileMetadata = props.externalReferenceMetadata.files[0]; + const file = { + id: fileId, + ...fileMetadata, + }; + + return { + event: , + timelineAvatar: isImage(file) ? 'image' : 'document', + getActions: () => getFileAttachmentActions({ caseId, fileId }), + hideDefaultActions: true, + }; +}; + +export const getFileType = (): ExternalReferenceAttachmentType => ({ + id: FILE_ATTACHMENT_TYPE, + icon: 'document', + displayName: 'File Attachment Type', + getAttachmentViewObject: getFileAttachmentViewObject, +}); diff --git a/x-pack/plugins/cases/public/components/files/files_table.test.tsx b/x-pack/plugins/cases/public/components/files/files_table.test.tsx new file mode 100644 index 0000000000000..651f86e76b462 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_table.test.tsx @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor, within } from '@testing-library/react'; + +import { basicFileMock } from '../../containers/mock'; +import type { AppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/files'; +import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock'; +import { FilesTable } from './files_table'; +import userEvent from '@testing-library/user-event'; + +describe('FilesTable', () => { + const onTableChange = jest.fn(); + const defaultProps = { + caseId: 'foobar', + items: [basicFileMock], + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, + isLoading: false, + onChange: onTableChange, + }; + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-results-count')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-filename')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-filetype')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-date-added')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + }); + + it('renders loading state', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-loading')).toBeInTheDocument(); + }); + + it('renders empty table', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); + }); + + it('FileAdd in empty table is clickable', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); + + const addFileButton = await screen.findByTestId('cases-files-add'); + + expect(addFileButton).toBeInTheDocument(); + + userEvent.click(addFileButton); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + }); + + it('renders single result count properly', async () => { + const mockPagination = { pageIndex: 0, pageSize: 10, totalItemCount: 1 }; + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-results-count')).toHaveTextContent( + `Showing ${defaultProps.items.length} file` + ); + }); + + it('non image rows dont open file preview', async () => { + const nonImageFileMock = { ...basicFileMock, mimeType: 'something/else' }; + + appMockRender.render(); + + userEvent.click( + await within(await screen.findByTestId('cases-files-table-filename')).findByTitle( + 'No preview available' + ) + ); + + expect(await screen.queryByTestId('cases-files-image-preview')).not.toBeInTheDocument(); + }); + + it('image rows open file preview', async () => { + appMockRender.render(); + + userEvent.click( + await screen.findByRole('button', { + name: `${basicFileMock.name}.${basicFileMock.extension}`, + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); + + it('different mimeTypes are displayed correctly', async () => { + const mockPagination = { pageIndex: 0, pageSize: 10, totalItemCount: 7 }; + appMockRender.render( + + ); + + expect((await screen.findAllByText('Unknown')).length).toBe(4); + expect(await screen.findByText('Compressed')).toBeInTheDocument(); + expect(await screen.findByText('Text')).toBeInTheDocument(); + expect(await screen.findByText('Image')).toBeInTheDocument(); + }); + + it('download button renders correctly', async () => { + appMockRender.render(); + + expect(appMockRender.getFilesClient().getDownloadHref).toBeCalledTimes(1); + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + }); + + it('delete button renders correctly', async () => { + appMockRender.render(); + + expect(appMockRender.getFilesClient().getDownloadHref).toBeCalledTimes(1); + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + }); + + it('clicking delete button opens deletion modal', async () => { + appMockRender.render(); + + expect(appMockRender.getFilesClient().getDownloadHref).toBeCalledTimes(1); + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + it('go to next page calls onTableChange with correct values', async () => { + const mockPagination = { pageIndex: 0, pageSize: 1, totalItemCount: 2 }; + + appMockRender.render( + + ); + + userEvent.click(await screen.findByTestId('pagination-button-next')); + + await waitFor(() => + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: mockPagination.pageIndex + 1, size: mockPagination.pageSize }, + }) + ); + }); + + it('go to previous page calls onTableChange with correct values', async () => { + const mockPagination = { pageIndex: 1, pageSize: 1, totalItemCount: 2 }; + + appMockRender.render( + + ); + + userEvent.click(await screen.findByTestId('pagination-button-previous')); + + await waitFor(() => + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: mockPagination.pageIndex - 1, size: mockPagination.pageSize }, + }) + ); + }); + + it('changing perPage calls onTableChange with correct values', async () => { + appMockRender.render( + + ); + + userEvent.click(screen.getByTestId('tablePaginationPopoverButton')); + + const pageSizeOption = screen.getByTestId('tablePagination-50-rows'); + + pageSizeOption.style.pointerEvents = 'all'; + + userEvent.click(pageSizeOption); + + await waitFor(() => + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: 0, size: 50 }, + }) + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/files_table.tsx b/x-pack/plugins/cases/public/components/files/files_table.tsx new file mode 100644 index 0000000000000..6433d90a91d44 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import type { Pagination, EuiBasicTableProps } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import { EuiBasicTable, EuiLoadingContent, EuiSpacer, EuiText, EuiEmptyPrompt } from '@elastic/eui'; + +import * as i18n from './translations'; +import { useFilesTableColumns } from './use_files_table_columns'; +import { FilePreview } from './file_preview'; +import { AddFile } from './add_file'; +import { useFilePreview } from './use_file_preview'; + +const EmptyFilesTable = ({ caseId }: { caseId: string }) => ( + {i18n.NO_FILES}} + data-test-subj="cases-files-table-empty" + titleSize="xs" + actions={} + /> +); + +EmptyFilesTable.displayName = 'EmptyFilesTable'; + +interface FilesTableProps { + caseId: string; + isLoading: boolean; + items: FileJSON[]; + onChange: EuiBasicTableProps['onChange']; + pagination: Pagination; +} + +export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: FilesTableProps) => { + const { isPreviewVisible, showPreview, closePreview } = useFilePreview(); + + const [selectedFile, setSelectedFile] = useState(); + + const displayPreview = (file: FileJSON) => { + setSelectedFile(file); + showPreview(); + }; + + const columns = useFilesTableColumns({ caseId, showPreview: displayPreview }); + + return isLoading ? ( + <> + + + + ) : ( + <> + {pagination.totalItemCount > 0 && ( + <> + + + {i18n.SHOWING_FILES(items.length)} + + + )} + + } + /> + {isPreviewVisible && selectedFile !== undefined && ( + + )} + + ); +}; + +FilesTable.displayName = 'FilesTable'; diff --git a/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx b/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx new file mode 100644 index 0000000000000..bfac1998a857a --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { FilesUtilityBar } from './files_utility_bar'; + +const defaultProps = { + caseId: 'foobar', + onSearch: jest.fn(), +}; + +describe('FilesUtilityBar', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-search')).toBeInTheDocument(); + }); + + it('search text passed correctly to callback', async () => { + appMockRender.render(); + + await userEvent.type(screen.getByTestId('cases-files-search'), 'My search{enter}'); + expect(defaultProps.onSearch).toBeCalledWith('My search'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx new file mode 100644 index 0000000000000..71b1ef503fc63 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch } from '@elastic/eui'; +import { AddFile } from './add_file'; + +import * as i18n from './translations'; + +interface FilesUtilityBarProps { + caseId: string; + onSearch: (newSearch: string) => void; +} + +export const FilesUtilityBar = ({ caseId, onSearch }: FilesUtilityBarProps) => { + return ( + + + + + + + ); +}; + +FilesUtilityBar.displayName = 'FilesUtilityBar'; diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx new file mode 100644 index 0000000000000..4023c5b18cea8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ACTIONS = i18n.translate('xpack.cases.caseView.files.actions', { + defaultMessage: 'Actions', +}); + +export const ADD_FILE = i18n.translate('xpack.cases.caseView.files.addFile', { + defaultMessage: 'Add File', +}); + +export const CLOSE_MODAL = i18n.translate('xpack.cases.caseView.files.closeModal', { + defaultMessage: 'Close', +}); + +export const DATE_ADDED = i18n.translate('xpack.cases.caseView.files.dateAdded', { + defaultMessage: 'Date Added', +}); + +export const DELETE_FILE = i18n.translate('xpack.cases.caseView.files.deleteFile', { + defaultMessage: 'Delete File', +}); + +export const DOWNLOAD_FILE = i18n.translate('xpack.cases.caseView.files.downloadFile', { + defaultMessage: 'Download File', +}); + +export const FILES_TABLE = i18n.translate('xpack.cases.caseView.files.filesTable', { + defaultMessage: 'Files table', +}); + +export const NAME = i18n.translate('xpack.cases.caseView.files.name', { + defaultMessage: 'Name', +}); + +export const NO_FILES = i18n.translate('xpack.cases.caseView.files.noFilesAvailable', { + defaultMessage: 'No files available', +}); + +export const NO_PREVIEW = i18n.translate('xpack.cases.caseView.files.noPreviewAvailable', { + defaultMessage: 'No preview available', +}); + +export const RESULTS_COUNT = i18n.translate('xpack.cases.caseView.files.resultsCount', { + defaultMessage: 'Showing', +}); + +export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { + defaultMessage: 'Type', +}); + +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseView.files.searchPlaceholder', { + defaultMessage: 'Search files', +}); + +export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.files.failedUpload', { + defaultMessage: 'Failed to upload file', +}); + +export const UNKNOWN_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.unknownMimeType', { + defaultMessage: 'Unknown', +}); + +export const IMAGE_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.imageMimeType', { + defaultMessage: 'Image', +}); + +export const TEXT_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.textMimeType', { + defaultMessage: 'Text', +}); + +export const COMPRESSED_MIME_TYPE = i18n.translate( + 'xpack.cases.caseView.files.compressedMimeType', + { + defaultMessage: 'Compressed', + } +); + +export const PDF_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.pdfMimeType', { + defaultMessage: 'PDF', +}); + +export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => + i18n.translate('xpack.cases.caseView.files.successfulUploadFileName', { + defaultMessage: 'File {fileName} uploaded successfully', + values: { fileName }, + }); + +export const SHOWING_FILES = (totalFiles: number) => + i18n.translate('xpack.cases.caseView.files.showingFilesTitle', { + values: { totalFiles }, + defaultMessage: 'Showing {totalFiles} {totalFiles, plural, =1 {file} other {files}}', + }); + +export const ADDED = i18n.translate('xpack.cases.caseView.files.added', { + defaultMessage: 'added ', +}); + +export const ADDED_UNKNOWN_FILE = i18n.translate('xpack.cases.caseView.files.addedUnknownFile', { + defaultMessage: 'added an unknown file', +}); + +export const DELETE = i18n.translate('xpack.cases.caseView.files.delete', { + defaultMessage: 'Delete', +}); + +export const DELETE_FILE_TITLE = i18n.translate('xpack.cases.caseView.files.deleteThisFile', { + defaultMessage: 'Delete this file?', +}); diff --git a/x-pack/plugins/cases/public/components/files/types.ts b/x-pack/plugins/cases/public/components/files/types.ts new file mode 100644 index 0000000000000..a211b5ac4053d --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as rt from 'io-ts'; + +import type { SingleFileAttachmentMetadataRt } from '../../../common/api'; + +export type DownloadableFile = rt.TypeOf & { id: string }; diff --git a/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx new file mode 100644 index 0000000000000..49e18fb818cd9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { useFilePreview } from './use_file_preview'; + +describe('useFilePreview', () => { + it('isPreviewVisible is false by default', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + }); + + it('showPreview sets isPreviewVisible to true', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + + act(() => { + result.current.showPreview(); + }); + + expect(result.current.isPreviewVisible).toBeTruthy(); + }); + + it('closePreview sets isPreviewVisible to false', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + + act(() => { + result.current.showPreview(); + }); + + expect(result.current.isPreviewVisible).toBeTruthy(); + + act(() => { + result.current.closePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/use_file_preview.tsx b/x-pack/plugins/cases/public/components/files/use_file_preview.tsx new file mode 100644 index 0000000000000..c802aa38fc688 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_file_preview.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState } from 'react'; + +export const useFilePreview = () => { + const [isPreviewVisible, setIsPreviewVisible] = useState(false); + + const closePreview = () => setIsPreviewVisible(false); + const showPreview = () => setIsPreviewVisible(true); + + return { isPreviewVisible, showPreview, closePreview }; +}; diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx new file mode 100644 index 0000000000000..77070da0dbc57 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FilesTableColumnsProps } from './use_files_table_columns'; +import { useFilesTableColumns } from './use_files_table_columns'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { renderHook } from '@testing-library/react-hooks'; +import { basicCase } from '../../containers/mock'; + +describe('useFilesTableColumns', () => { + let appMockRender: AppMockRenderer; + + const useFilesTableColumnsProps: FilesTableColumnsProps = { + caseId: basicCase.id, + showPreview: () => {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('return all files table columns correctly', async () => { + const { result } = renderHook(() => useFilesTableColumns(useFilesTableColumnsProps), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "cases-files-table-filename", + "name": "Name", + "render": [Function], + "width": "60%", + }, + Object { + "data-test-subj": "cases-files-table-filetype", + "name": "Type", + "render": [Function], + }, + Object { + "data-test-subj": "cases-files-table-date-added", + "dataType": "date", + "field": "created", + "name": "Date Added", + }, + Object { + "actions": Array [ + Object { + "description": "Download File", + "isPrimary": true, + "name": "Download", + "render": [Function], + }, + Object { + "description": "Delete File", + "isPrimary": true, + "name": "Delete", + "render": [Function], + }, + ], + "name": "Actions", + "width": "120px", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx new file mode 100644 index 0000000000000..80568189afb58 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { EuiBasicTableColumn } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import * as i18n from './translations'; +import { parseMimeType } from './utils'; +import { FileNameLink } from './file_name_link'; +import { FileDownloadButton } from './file_download_button'; +import { FileDeleteButton } from './file_delete_button'; + +export interface FilesTableColumnsProps { + caseId: string; + showPreview: (file: FileJSON) => void; +} + +export const useFilesTableColumns = ({ + caseId, + showPreview, +}: FilesTableColumnsProps): Array> => { + return [ + { + name: i18n.NAME, + 'data-test-subj': 'cases-files-table-filename', + render: (file: FileJSON) => ( + showPreview(file)} /> + ), + width: '60%', + }, + { + name: i18n.TYPE, + 'data-test-subj': 'cases-files-table-filetype', + render: (attachment: FileJSON) => { + return {parseMimeType(attachment.mimeType)}; + }, + }, + { + name: i18n.DATE_ADDED, + field: 'created', + 'data-test-subj': 'cases-files-table-date-added', + dataType: 'date', + }, + { + name: i18n.ACTIONS, + width: '120px', + actions: [ + { + name: 'Download', + isPrimary: true, + description: i18n.DOWNLOAD_FILE, + render: (file: FileJSON) => , + }, + { + name: 'Delete', + isPrimary: true, + description: i18n.DELETE_FILE, + render: (file: FileJSON) => ( + + ), + }, + ], + }, + ]; +}; diff --git a/x-pack/plugins/cases/public/components/files/utils.test.tsx b/x-pack/plugins/cases/public/components/files/utils.test.tsx new file mode 100644 index 0000000000000..411492d1a2bab --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/utils.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { JsonValue } from '@kbn/utility-types'; + +import { + compressionMimeTypes, + imageMimeTypes, + pdfMimeTypes, + textMimeTypes, +} from '../../../common/constants/mime_types'; +import { basicFileMock } from '../../containers/mock'; +import { isImage, isValidFileExternalReferenceMetadata, parseMimeType } from './utils'; + +describe('isImage', () => { + it.each(imageMimeTypes)('should return true for image mime type: %s', (mimeType) => { + expect(isImage({ mimeType })).toBeTruthy(); + }); + + it.each(textMimeTypes)('should return false for text mime type: %s', (mimeType) => { + expect(isImage({ mimeType })).toBeFalsy(); + }); +}); + +describe('parseMimeType', () => { + it('should return Unknown for empty strings', () => { + expect(parseMimeType('')).toBe('Unknown'); + }); + + it('should return Unknown for undefined', () => { + expect(parseMimeType(undefined)).toBe('Unknown'); + }); + + it('should return Unknown for strings starting with forward slash', () => { + expect(parseMimeType('/start')).toBe('Unknown'); + }); + + it('should return Unknown for strings with no forward slash', () => { + expect(parseMimeType('no-slash')).toBe('Unknown'); + }); + + it('should return capitalize first letter for valid strings', () => { + expect(parseMimeType('foo/bar')).toBe('Foo'); + }); + + it.each(imageMimeTypes)('should return "Image" for image mime type: %s', (mimeType) => { + expect(parseMimeType(mimeType)).toBe('Image'); + }); + + it.each(textMimeTypes)('should return "Text" for text mime type: %s', (mimeType) => { + expect(parseMimeType(mimeType)).toBe('Text'); + }); + + it.each(compressionMimeTypes)( + 'should return "Compressed" for image mime type: %s', + (mimeType) => { + expect(parseMimeType(mimeType)).toBe('Compressed'); + } + ); + + it.each(pdfMimeTypes)('should return "Pdf" for text mime type: %s', (mimeType) => { + expect(parseMimeType(mimeType)).toBe('PDF'); + }); +}); + +describe('isValidFileExternalReferenceMetadata', () => { + it('should return false for empty objects', () => { + expect(isValidFileExternalReferenceMetadata({})).toBeFalsy(); + }); + + it('should return false if the files property is missing', () => { + expect(isValidFileExternalReferenceMetadata({ foo: 'bar' })).toBeFalsy(); + }); + + it('should return false if the files property is not an array', () => { + expect(isValidFileExternalReferenceMetadata({ files: 'bar' })).toBeFalsy(); + }); + + it('should return false if files is not an array of file metadata', () => { + expect(isValidFileExternalReferenceMetadata({ files: [3] })).toBeFalsy(); + }); + + it('should return false if files is not an array of file metadata 2', () => { + expect( + isValidFileExternalReferenceMetadata({ files: [{ name: 'foo', mimeType: 'bar' }] }) + ).toBeFalsy(); + }); + + it('should return true if the metadata is as expected', () => { + expect( + isValidFileExternalReferenceMetadata({ files: [basicFileMock as unknown as JsonValue] }) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/utils.tsx b/x-pack/plugins/cases/public/components/files/utils.tsx new file mode 100644 index 0000000000000..b870c733eb10e --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/utils.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CommentRequestExternalReferenceType, + FileAttachmentMetadata, +} from '../../../common/api'; + +import { + compressionMimeTypes, + imageMimeTypes, + textMimeTypes, + pdfMimeTypes, +} from '../../../common/constants/mime_types'; +import { FileAttachmentMetadataRt } from '../../../common/api'; +import * as i18n from './translations'; + +export const isImage = (file: { mimeType?: string }) => file.mimeType?.startsWith('image/'); + +export const parseMimeType = (mimeType: string | undefined) => { + if (typeof mimeType === 'undefined') { + return i18n.UNKNOWN_MIME_TYPE; + } + + if (imageMimeTypes.includes(mimeType)) { + return i18n.IMAGE_MIME_TYPE; + } + + if (textMimeTypes.includes(mimeType)) { + return i18n.TEXT_MIME_TYPE; + } + + if (compressionMimeTypes.includes(mimeType)) { + return i18n.COMPRESSED_MIME_TYPE; + } + + if (pdfMimeTypes.includes(mimeType)) { + return i18n.PDF_MIME_TYPE; + } + + const result = mimeType.split('/'); + + if (result.length <= 1 || result[0] === '') { + return i18n.UNKNOWN_MIME_TYPE; + } + + return result[0].charAt(0).toUpperCase() + result[0].slice(1); +}; + +export const isValidFileExternalReferenceMetadata = ( + externalReferenceMetadata: CommentRequestExternalReferenceType['externalReferenceMetadata'] +): externalReferenceMetadata is FileAttachmentMetadata => { + return ( + FileAttachmentMetadataRt.is(externalReferenceMetadata) && + externalReferenceMetadata?.files?.length >= 1 + ); +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx index 1d6a5ec8ca11a..84dddd64ba61b 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx @@ -9,6 +9,8 @@ import { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; + +import { AttachmentActionType } from '../../../../client/attachment_framework/types'; import { useKibana } from '../../../../common/lib/kibana'; import { parseCommentString, @@ -42,6 +44,7 @@ export const useLensOpenVisualization = ({ comment }: { comment: string }) => { actionConfig: !lensVisualization.length ? null : { + type: AttachmentActionType.BUTTON as const, iconType: 'lensApp', label: i18n.translate( 'xpack.cases.markdownEditor.plugins.lens.openVisualizationButtonLabel', diff --git a/x-pack/plugins/cases/public/components/property_actions/index.tsx b/x-pack/plugins/cases/public/components/property_actions/index.tsx index 4de52d551bf2f..833ace8333d2b 100644 --- a/x-pack/plugins/cases/public/components/property_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/property_actions/index.tsx @@ -9,6 +9,9 @@ import React, { useCallback, useState } from 'react'; import type { EuiButtonProps } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiPopover, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; +import type { AttachmentAction } from '../../client/attachment_framework/types'; + +import { AttachmentActionType } from '../../client/attachment_framework/types'; import * as i18n from './translations'; export interface PropertyActionButtonProps { @@ -45,7 +48,7 @@ const PropertyActionButton = React.memo( PropertyActionButton.displayName = 'PropertyActionButton'; export interface PropertyActionsProps { - propertyActions: PropertyActionButtonProps[]; + propertyActions: AttachmentAction[]; customDataTestSubj?: string; } @@ -93,14 +96,17 @@ export const PropertyActions = React.memo( {propertyActions.map((action, key) => ( - onClosePopover(action.onClick)} - customDataTestSubj={customDataTestSubj} - /> + {(action.type === AttachmentActionType.BUTTON && ( + onClosePopover(action.onClick)} + customDataTestSubj={customDataTestSubj} + /> + )) || + (action.type === AttachmentActionType.CUSTOM && action.render())} ))} diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx index db21c2f2100c6..4cf6c9844e948 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx @@ -36,6 +36,7 @@ import { useCaseViewNavigation, useCaseViewParams } from '../../../common/naviga import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../../client/attachment_framework/persistable_state_registry'; import { userProfiles } from '../../../containers/user_profiles/api.mock'; +import { AttachmentActionType } from '../../../client/attachment_framework/types'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/navigation/hooks'); @@ -849,9 +850,27 @@ describe('createCommentUserActionBuilder', () => { const attachment = getExternalReferenceAttachment({ getActions: () => [ - { label: 'My primary button', isPrimary: true, iconType: 'danger', onClick }, - { label: 'My primary 2 button', isPrimary: true, iconType: 'danger', onClick }, - { label: 'My primary 3 button', isPrimary: true, iconType: 'danger', onClick }, + { + type: AttachmentActionType.BUTTON as const, + label: 'My primary button', + isPrimary: true, + iconType: 'danger', + onClick, + }, + { + type: AttachmentActionType.BUTTON as const, + label: 'My primary 2 button', + isPrimary: true, + iconType: 'danger', + onClick, + }, + { + type: AttachmentActionType.BUTTON as const, + label: 'My primary 3 button', + isPrimary: true, + iconType: 'danger', + onClick, + }, ], }); @@ -888,14 +907,75 @@ describe('createCommentUserActionBuilder', () => { expect(onClick).toHaveBeenCalledTimes(2); }); + it('shows correctly a custom action', async () => { + const onClick = jest.fn(); + + const attachment = getExternalReferenceAttachment({ + getActions: () => [ + { + type: AttachmentActionType.CUSTOM as const, + isPrimary: true, + label: 'Test button', + render: () => ( + , style: { - height: 0, + minHeight: 0, width: 0, }, }, @@ -1100,12 +1100,12 @@ describe('DragDrop', () => { expect( component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(0).prop('style') ).toEqual({ - transform: 'translateY(-8px)', + transform: 'translateY(-4px)', }); expect( component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(1).prop('style') ).toEqual({ - transform: 'translateY(-8px)', + transform: 'translateY(-4px)', }); component @@ -1258,12 +1258,12 @@ describe('DragDrop', () => { expect( component.find('[data-test-subj="testDragDrop-reorderableDrag"]').at(0).prop('style') ).toEqual({ - transform: 'translateY(+8px)', + transform: 'translateY(+4px)', }); expect( component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(0).prop('style') ).toEqual({ - transform: 'translateY(-40px)', + transform: 'translateY(-32px)', }); expect( component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(1).prop('style') diff --git a/packages/kbn-dom-drag-drop/src/drag_drop.tsx b/packages/kbn-dom-drag-drop/src/drag_drop.tsx index 47909a818cea9..ce3d6147d813f 100644 --- a/packages/kbn-dom-drag-drop/src/drag_drop.tsx +++ b/packages/kbn-dom-drag-drop/src/drag_drop.tsx @@ -24,6 +24,7 @@ import { Ghost, } from './providers'; import { DropType } from './types'; +import { REORDER_ITEM_MARGIN } from './constants'; import './sass/drag_drop.scss'; /** @@ -71,11 +72,15 @@ interface BaseProps { * The React element which will be passed the draggable handlers */ children: ReactElement; + + /** + * Disable any drag & drop behaviour + */ + isDisabled?: boolean; /** * Indicates whether or not this component is draggable. */ draggable?: boolean; - /** * Additional class names to apply when another element is over the drop target */ @@ -152,7 +157,7 @@ interface DropsInnerProps extends BaseProps { isNotDroppable: boolean; } -const lnsLayerPanelDimensionMargin = 8; +const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2; /** * DragDrop component @@ -174,6 +179,10 @@ export const DragDrop = (props: BaseProps) => { onTrackUICounterEvent, } = useContext(DragContext); + if (props.isDisabled) { + return props.children; + } + const { value, draggable, dropTypes, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); @@ -358,7 +367,7 @@ const DragInner = memo(function DragInner({ ghost: keyboardModeOn ? { children, - style: { width: currentTarget.offsetWidth, height: currentTarget.offsetHeight }, + style: { width: currentTarget.offsetWidth, minHeight: currentTarget?.offsetHeight }, } : undefined, }); @@ -417,12 +426,14 @@ const DragInner = memo(function DragInner({ keyboardMode && activeDropTarget && activeDropTarget.dropType !== 'reorder'; + return (
@@ -772,7 +783,7 @@ const ReorderableDrag = memo(function ReorderableDrag( | KeyboardEvent['currentTarget'] ) => { if (currentTarget) { - const height = currentTarget.offsetHeight + lnsLayerPanelDimensionMargin; + const height = currentTarget.offsetHeight + REORDER_OFFSET; setReorderState((s: ReorderState) => ({ ...s, draggingHeight: height, @@ -875,7 +886,7 @@ const ReorderableDrag = memo(function ReorderableDrag( areItemsReordered ? { transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce( - (acc, cur) => acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin, + (acc, cur) => acc + Number(cur.height || 0) + REORDER_OFFSET, 0 )}px)`, } diff --git a/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx b/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx new file mode 100644 index 0000000000000..590106157f304 --- /dev/null +++ b/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import classnames from 'classnames'; + +/** + * DropOverlayWrapper Props + */ +export interface DropOverlayWrapperProps { + isVisible: boolean; + className?: string; + overlayProps?: object; +} + +/** + * This prevents the in-place droppable styles (under children) and allows to rather show an overlay with droppable styles (on top of children) + * @param isVisible + * @param children + * @param overlayProps + * @param className + * @param otherProps + * @constructor + */ +export const DropOverlayWrapper: React.FC = ({ + isVisible, + children, + overlayProps, + className, + ...otherProps +}) => { + return ( +
+ {children} + {isVisible && ( +
+ )} +
+ ); +}; diff --git a/packages/kbn-dom-drag-drop/src/index.ts b/packages/kbn-dom-drag-drop/src/index.ts index 340dba349612c..9fa4f37103522 100644 --- a/packages/kbn-dom-drag-drop/src/index.ts +++ b/packages/kbn-dom-drag-drop/src/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './providers'; export * from './drag_drop'; +export { DropOverlayWrapper, type DropOverlayWrapperProps } from './drop_overlay_wrapper'; diff --git a/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx b/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx index 5dc83dbce915f..c2aa11eb8bc8d 100644 --- a/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx +++ b/packages/kbn-dom-drag-drop/src/providers/reorder_provider.tsx @@ -8,7 +8,7 @@ import React, { useState, useMemo } from 'react'; import classNames from 'classnames'; -import { DEFAULT_DATA_TEST_SUBJ } from '../constants'; +import { DEFAULT_DATA_TEST_SUBJ, REORDER_ITEM_HEIGHT } from '../constants'; /** * Reorder state @@ -54,7 +54,7 @@ export const ReorderContext = React.createContext({ reorderState: { reorderedItems: [], direction: '-', - draggingHeight: 40, + draggingHeight: REORDER_ITEM_HEIGHT, isReorderOn: false, groupId: '', }, @@ -66,6 +66,7 @@ export const ReorderContext = React.createContext({ * @param id * @param children * @param className + * @param draggingHeight * @param dataTestSubj * @constructor */ @@ -73,17 +74,19 @@ export function ReorderProvider({ id, children, className, + draggingHeight = REORDER_ITEM_HEIGHT, dataTestSubj = DEFAULT_DATA_TEST_SUBJ, }: { id: string; children: React.ReactNode; className?: string; + draggingHeight?: number; dataTestSubj?: string; }) { const [state, setState] = useState({ reorderedItems: [], direction: '-', - draggingHeight: 40, + draggingHeight, isReorderOn: false, groupId: id, }); diff --git a/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss b/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss index d5587ae8ec6a4..56ce648266e7b 100644 --- a/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss +++ b/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss @@ -1,7 +1,6 @@ @import './drag_drop_mixins'; .domDragDrop { - user-select: none; transition: $euiAnimSpeedFast ease-in-out; transition-property: background-color, border-color, opacity; z-index: $domDragDropZLevel1; @@ -12,11 +11,11 @@ border: $euiBorderWidthThin dashed $euiBorderColor; position: absolute !important; // sass-lint:disable-line no-important margin: 0 !important; // sass-lint:disable-line no-important - bottom: 100%; + top: 0; width: 100%; left: 0; opacity: .9; - transform: translate(-12px, 8px); + transform: translate($euiSizeXS, $euiSizeXS); z-index: $domDragDropZLevel3; pointer-events: none; outline: $euiFocusRingSize solid currentColor; // Safari & Firefox @@ -29,7 +28,8 @@ @include mixinDomDragDropHover; // Include a possible nested button like when using FieldButton - > .kbnFieldButton__button { + & .kbnFieldButton__button, + & .euiLink { cursor: grab; } @@ -39,14 +39,17 @@ } // Drop area -.domDragDrop-isDroppable { +.domDragDrop-isDroppable:not(.domDragDrop__dropOverlayWrapper) { @include mixinDomDroppable; } // Drop area when there's an item being dragged .domDragDrop-isDropTarget { - @include mixinDomDroppable; - @include mixinDomDroppableActive; + &:not(.domDragDrop__dropOverlayWrapper) { + @include mixinDomDroppable; + @include mixinDomDroppableActive; + } + > * { pointer-events: none; } @@ -57,7 +60,7 @@ } // Drop area while hovering with item -.domDragDrop-isActiveDropTarget { +.domDragDrop-isActiveDropTarget:not(.domDragDrop__dropOverlayWrapper) { z-index: $domDragDropZLevel3; @include mixinDomDroppableActiveHover; } @@ -72,9 +75,9 @@ text-decoration: line-through; } -.domDragDrop-notCompatible { +.domDragDrop-notCompatible:not(.domDragDrop__dropOverlayWrapper) { background-color: $euiColorHighlight !important; - border: $euiBorderWidthThin dashed $euiBorderColor !important; + border: $euiBorderWidthThin dashed $euiColorVis5 !important; &.domDragDrop-isActiveDropTarget { background-color: rgba(251, 208, 17, .25) !important; border-color: $euiColorVis5 !important; @@ -91,12 +94,12 @@ } } -$lnsLayerPanelDimensionMargin: 8px; +$reorderItemMargin: $euiSizeS; .domDragDrop__reorderableDrop { position: absolute; width: 100%; top: 0; - height: calc(100% + #{$lnsLayerPanelDimensionMargin}); + height: calc(100% + #{$reorderItemMargin / 2}); } .domDragDrop-translatableDrop { @@ -146,6 +149,10 @@ $lnsLayerPanelDimensionMargin: 8px; } } +.domDragDrop--isDragStarted { + opacity: .5; +} + .domDragDrop__extraDrops { opacity: 0; visibility: hidden; @@ -193,7 +200,7 @@ $lnsLayerPanelDimensionMargin: 8px; .domDragDrop__extraDrop { position: relative; - height: $euiSizeXS * 10; + height: $euiSizeXS * 8; min-width: $euiSize * 7; color: $euiColorSuccessText; padding: $euiSizeXS; @@ -201,3 +208,28 @@ $lnsLayerPanelDimensionMargin: 8px; color: $euiColorWarningText; } } + +.domDragDrop__dropOverlayWrapper { + position: relative; + height: 100%; +} + +.domDragDrop__dropOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: $domDragDropZLevel3; + transition: $euiAnimSpeedFast ease-in-out; + transition-property: background-color, border-color, opacity; + + .domDragDrop-isDropTarget & { + @include mixinDomDroppable($euiBorderWidthThick); + @include mixinDomDroppableActive($euiBorderWidthThick); + } + + .domDragDrop-isActiveDropTarget & { + @include mixinDomDroppableActiveHover($euiBorderWidthThick); + } +} diff --git a/packages/kbn-dom-drag-drop/src/sass/drag_drop_mixins.scss b/packages/kbn-dom-drag-drop/src/sass/drag_drop_mixins.scss index fbd460a67f7d0..fb495b4fcfba4 100644 --- a/packages/kbn-dom-drag-drop/src/sass/drag_drop_mixins.scss +++ b/packages/kbn-dom-drag-drop/src/sass/drag_drop_mixins.scss @@ -13,31 +13,32 @@ $domDragDropZLevel3: 3; @mixin mixinDomDraggable { @include euiSlightShadow; background: $euiColorEmptyShade; - border: $euiBorderWidthThin dashed transparent; cursor: grab; } // Static styles for a drop area -@mixin mixinDomDroppable { - border: $euiBorderWidthThin dashed $euiBorderColor !important; +@mixin mixinDomDroppable($borderWidth: $euiBorderWidthThin) { + border: $borderWidth dashed transparent; } // Hovering state for drag item and drop area @mixin mixinDomDragDropHover { &:hover { - border: $euiBorderWidthThin dashed $euiColorMediumShade !important; + transform: translateX($euiSizeXS); + transition: transform $euiAnimSpeedSlow ease-out; } } // Style for drop area when there's an item being dragged -@mixin mixinDomDroppableActive { +@mixin mixinDomDroppableActive($borderWidth: $euiBorderWidthThin) { background-color: transparentize($euiColorVis0, .9) !important; + border: $borderWidth dashed $euiColorVis0 !important; } // Style for drop area while hovering with item -@mixin mixinDomDroppableActiveHover { +@mixin mixinDomDroppableActiveHover($borderWidth: $euiBorderWidthThin) { background-color: transparentize($euiColorVis0, .75) !important; - border: $euiBorderWidthThin dashed $euiColorVis0 !important; + border: $borderWidth dashed $euiColorVis0 !important; } // Style for drop area that is not allowed for current item diff --git a/packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap b/packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap index e65b5fcb8fbbd..0b4ceaf06e863 100644 --- a/packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap +++ b/packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap @@ -2,7 +2,7 @@ exports[`fieldAction is rendered 1`] = `
@@ -50,7 +58,7 @@ exports[`fieldIcon is rendered 1`] = ` exports[`isActive defaults to false 1`] = `
@@ -67,7 +79,7 @@ exports[`isActive defaults to false 1`] = ` exports[`isActive renders true 1`] = `
`; -exports[`isDraggable is rendered 1`] = ` +exports[`sizes s is applied 1`] = `
`; -exports[`sizes m is applied 1`] = ` +exports[`sizes xs is applied 1`] = `
`; -exports[`sizes s is applied 1`] = ` +exports[`with drag handle is rendered 1`] = `
+
+ + drag + +
diff --git a/packages/kbn-react-field/src/field_button/field_button.scss b/packages/kbn-react-field/src/field_button/field_button.scss index f71e097ab7138..5fd87c38b930e 100644 --- a/packages/kbn-react-field/src/field_button/field_button.scss +++ b/packages/kbn-react-field/src/field_button/field_button.scss @@ -2,8 +2,9 @@ @include euiFontSizeS; border-radius: $euiBorderRadius; margin-bottom: $euiSizeXS; + padding: 0 $euiSizeS; display: flex; - align-items: center; + align-items: flex-start; transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance, background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation @@ -13,29 +14,10 @@ } } -.kbnFieldButton--isDraggable { - background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); - - &:hover, - &:focus, - &:focus-within { - @include euiBottomShadowMedium; - border-radius: $euiBorderRadius; - z-index: 2; - } - - .kbnFieldButton__button { - &:hover, - &:focus { - cursor: grab; - } - } -} - .kbnFieldButton__button { flex-grow: 1; text-align: left; - padding: $euiSizeS; + padding: $euiSizeS 0; display: flex; align-items: flex-start; line-height: normal; @@ -44,33 +26,55 @@ .kbnFieldButton__fieldIcon { flex-shrink: 0; line-height: 0; - margin-right: $euiSizeS; } .kbnFieldButton__name { flex-grow: 1; word-break: break-word; + padding: 0 $euiSizeS; } .kbnFieldButton__infoIcon { + display: flex; + align-items: center; + justify-content: center; + min-height: $euiSize; flex-shrink: 0; - margin-left: $euiSizeXS; + line-height: 0; } .kbnFieldButton__fieldAction { + margin-left: $euiSizeS; +} + +.kbnFieldButton__dragHandle { margin-right: $euiSizeS; } +.kbnFieldButton__fieldAction, +.kbnFieldButton__dragHandle { + line-height: $euiSizeXL; +} + // Reduce text size and spacing for the small size -.kbnFieldButton--small { +.kbnFieldButton--xs { font-size: $euiFontSizeXS; .kbnFieldButton__button { - padding: $euiSizeXS; + padding: $euiSizeXS 0; } - .kbnFieldButton__fieldIcon, .kbnFieldButton__fieldAction { - margin-right: $euiSizeXS; + margin-left: $euiSizeXS; + } + + .kbnFieldButton__fieldAction, + .kbnFieldButton__dragHandle { + line-height: $euiSizeL; } } + +.kbnFieldButton--flushBoth { + padding-left: 0; + padding-right: 0; +} diff --git a/packages/kbn-react-field/src/field_button/field_button.test.tsx b/packages/kbn-react-field/src/field_button/field_button.test.tsx index e61b41187ba7e..270144c7d346b 100644 --- a/packages/kbn-react-field/src/field_button/field_button.test.tsx +++ b/packages/kbn-react-field/src/field_button/field_button.test.tsx @@ -21,9 +21,11 @@ describe('sizes', () => { }); }); -describe('isDraggable', () => { +describe('with drag handle', () => { it('is rendered', () => { - const component = shallow(); + const component = shallow( + drag} /> + ); expect(component).toMatchSnapshot(); }); }); @@ -31,7 +33,7 @@ describe('isDraggable', () => { describe('fieldIcon', () => { it('is rendered', () => { const component = shallow( - fieldIcon} /> + fieldIcon} /> ); expect(component).toMatchSnapshot(); }); @@ -40,7 +42,12 @@ describe('fieldIcon', () => { describe('fieldAction', () => { it('is rendered', () => { const component = shallow( - fieldAction} /> + fieldAction} + /> ); expect(component).toMatchSnapshot(); }); @@ -48,11 +55,11 @@ describe('fieldAction', () => { describe('isActive', () => { it('defaults to false', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); it('renders true', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); }); diff --git a/packages/kbn-react-field/src/field_button/field_button.tsx b/packages/kbn-react-field/src/field_button/field_button.tsx index 4871fea049710..8d87e0e04cb63 100644 --- a/packages/kbn-react-field/src/field_button/field_button.tsx +++ b/packages/kbn-react-field/src/field_button/field_button.tsx @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import './field_button.scss'; import classNames from 'classnames'; import React, { ReactNode, HTMLAttributes, ButtonHTMLAttributes } from 'react'; import { CommonProps } from '@elastic/eui'; +import './field_button.scss'; export interface FieldButtonProps extends HTMLAttributes { /** @@ -34,13 +34,20 @@ export interface FieldButtonProps extends HTMLAttributes { */ isActive?: boolean; /** - * Styles the component differently to indicate it is draggable + * Custom drag handle element + */ + dragHandle?: React.ReactElement; + /** + * Use the xs size in condensed areas */ - isDraggable?: boolean; + size: ButtonSize; /** - * Use the small size in condensed areas + * Whether to skip side paddings + */ + flush?: 'both'; + /** + * Custom class name */ - size?: ButtonSize; className?: string; /** * The component will render a `