From 9186171ad160ead3fb9ec977f3465904fd293aa0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 6 Aug 2020 14:02:37 +0200 Subject: [PATCH 01/31] [Lens] Document UI terminology (#72423) --- x-pack/plugins/lens/layout.png | Bin 0 -> 487029 bytes x-pack/plugins/lens/readme.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 x-pack/plugins/lens/layout.png diff --git a/x-pack/plugins/lens/layout.png b/x-pack/plugins/lens/layout.png new file mode 100644 index 0000000000000000000000000000000000000000..170324a2ba393980e24762c0a2d0c5a5653d6420 GIT binary patch literal 487029 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 Date: Thu, 6 Aug 2020 15:07:14 +0200 Subject: [PATCH 02/31] Disable in-chart "Explore underlying data" by default (#74332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 disable in-chart "Explore underlying data" by default * test: 💍 disable in-chart action functional test suite Co-authored-by: Elastic Machine --- docs/drilldowns/explore-underlying-data.asciidoc | 4 ++-- x-pack/plugins/discover_enhanced/server/config.ts | 2 +- x-pack/test/functional/apps/dashboard/drilldowns/index.ts | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/drilldowns/explore-underlying-data.asciidoc b/docs/drilldowns/explore-underlying-data.asciidoc index e0f940f73e96e9..c2bba599730d82 100644 --- a/docs/drilldowns/explore-underlying-data.asciidoc +++ b/docs/drilldowns/explore-underlying-data.asciidoc @@ -33,9 +33,9 @@ applies the filters and time range created by the events that triggered the acti [role="screenshot"] image::images/explore_data_in_chart.png[Explore underlying data from chart] -You can disable this action by adding the following line to your `kibana.yml` config. +To enable this action add the following line to your `kibana.yml` config. ["source","yml"] ----------- -xpack.discoverEnhanced.actions.exploreDataInChart.enabled: false +xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true ----------- diff --git a/x-pack/plugins/discover_enhanced/server/config.ts b/x-pack/plugins/discover_enhanced/server/config.ts index becbdee1bfe408..3e5e29e8c7de70 100644 --- a/x-pack/plugins/discover_enhanced/server/config.ts +++ b/x-pack/plugins/discover_enhanced/server/config.ts @@ -10,7 +10,7 @@ import { PluginConfigDescriptor } from '../../../../src/core/server'; export const configSchema = schema.object({ actions: schema.object({ exploreDataInChart: schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.boolean({ defaultValue: false }), }), }), }); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 4cdb33c06947fe..ff604b18e1d517 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -24,6 +24,9 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_drilldowns')); loadTestFile(require.resolve('./explore_data_panel_action')); - loadTestFile(require.resolve('./explore_data_chart_action')); + + // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled + // setting set in kibana.yml to work. Once that is enabled by default, we can re-enable this test suite. + // loadTestFile(require.resolve('./explore_data_chart_action')); }); } From 152f41c3b04a34c712468c039928bb2a37c36aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 6 Aug 2020 15:18:49 +0200 Subject: [PATCH 03/31] [Logs UI] Replace deprecated getInjectedVar with NP spaces API (#74280) * Replace getInjectedVar() with NP spaces API * Fix typo in comment * Fix typo in comment Co-authored-by: Elastic Machine --- .../infra/public/apps/common_providers.tsx | 25 +++++++----- .../infra/public/components/loading_page.tsx | 4 +- .../plugins/infra/public/hooks/use_kibana.ts | 25 ++++++++++++ .../infra/public/hooks/use_kibana_space.ts | 40 +++++++++++++++++++ .../log_entry_categories/page_providers.tsx | 19 ++++++--- .../logs/log_entry_rate/page_providers.tsx | 15 +++++-- x-pack/plugins/infra/public/types.ts | 19 +++++---- .../infra/public/utils/use_kibana_space_id.ts | 31 -------------- 8 files changed, 117 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/infra/public/hooks/use_kibana.ts create mode 100644 x-pack/plugins/infra/public/hooks/use_kibana_space.ts delete mode 100644 x-pack/plugins/infra/public/utils/use_kibana_space_id.ts diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 9e4917856d8b26..fc82f4bf6cb00b 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -4,19 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { CoreStart } from 'kibana/public'; import { ApolloClient } from 'apollo-client'; -import { - useUiSetting$, - KibanaContextProvider, -} from '../../../../../src/plugins/kibana_react/public'; -import { TriggersActionsProvider } from '../utils/triggers_actions_context'; -import { InfraClientStartDeps } from '../types'; +import { CoreStart } from 'kibana/public'; +import React, { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../observability/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; +import { createKibanaContextForPlugin } from '../hooks/use_kibana'; +import { InfraClientStartDeps } from '../types'; import { ApolloClientContext } from '../utils/apollo_context'; -import { EuiThemeProvider } from '../../../observability/public'; import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; +import { TriggersActionsProvider } from '../utils/triggers_actions_context'; export const CommonInfraProviders: React.FC<{ apolloClient: ApolloClient<{}>; @@ -39,9 +37,14 @@ export const CoreProviders: React.FC<{ core: CoreStart; plugins: InfraClientStartDeps; }> = ({ children, core, plugins }) => { + const { Provider: KibanaContextProviderForPlugin } = useMemo( + () => createKibanaContextForPlugin(core, plugins), + [core, plugins] + ); + return ( - + {children} - + ); }; diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index 9d37fed45b583a..c410f37e7bf6ba 100644 --- a/x-pack/plugins/infra/public/components/loading_page.tsx +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -11,12 +11,12 @@ import { EuiPageBody, EuiPageContent, } from '@elastic/eui'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { FlexPage } from './page'; interface LoadingPageProps { - message?: string | JSX.Element; + message?: ReactNode; } export const LoadingPage = ({ message }: LoadingPageProps) => ( diff --git a/x-pack/plugins/infra/public/hooks/use_kibana.ts b/x-pack/plugins/infra/public/hooks/use_kibana.ts new file mode 100644 index 00000000000000..24511014d1a065 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_kibana.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from '../../../../../src/core/public'; +import { + createKibanaReactContext, + KibanaReactContextValue, + useKibana, +} from '../../../../../src/plugins/kibana_react/public'; +import { InfraClientStartDeps } from '../types'; + +export type PluginKibanaContextValue = CoreStart & InfraClientStartDeps; + +export const createKibanaContextForPlugin = (core: CoreStart, pluginsStart: InfraClientStartDeps) => + createKibanaReactContext({ + ...core, + ...pluginsStart, + }); + +export const useKibanaContextForPlugin = useKibana as () => KibanaReactContextValue< + PluginKibanaContextValue +>; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts new file mode 100644 index 00000000000000..1c062630089619 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_kibana_space.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsync } from 'react-use'; +import { useKibanaContextForPlugin } from '../hooks/use_kibana'; +import type { Space } from '../../../spaces/public'; + +export type ActiveSpace = + | { isLoading: true; error: undefined; space: undefined } + | { isLoading: false; error: Error; space: undefined } + | { isLoading: false; error: undefined; space: Space }; + +export const useActiveKibanaSpace = (): ActiveSpace => { + const kibana = useKibanaContextForPlugin(); + + const asyncActiveSpace = useAsync(kibana.services.spaces.getActiveSpace); + + if (asyncActiveSpace.loading) { + return { + isLoading: true, + error: undefined, + space: undefined, + }; + } else if (asyncActiveSpace.error) { + return { + isLoading: false, + error: asyncActiveSpace.error, + space: undefined, + }; + } else { + return { + isLoading: false, + error: undefined, + space: asyncActiveSpace.value!, + }; + } +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx index 48ad156714ccfd..723d833799e298 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx @@ -7,18 +7,25 @@ import React from 'react'; import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; +import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, sourceConfiguration } = useLogSourceContext(); - const spaceId = useKibanaSpaceId(); + const { sourceConfiguration, sourceId } = useLogSourceContext(); + const { space } = useActiveKibanaSpace(); + + // This is a rather crude way of guarding the dependent providers against + // arguments that are only made available asynchronously. Ideally, we'd use + // React concurrent mode and Suspense in order to handle that more gracefully. + if (sourceConfiguration?.configuration.logAlias == null || space == null) { + return null; + } return ( {children} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index ac11260d2075d5..e986fa37c2b2c8 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -9,23 +9,30 @@ import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; +import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { const { sourceId, sourceConfiguration } = useLogSourceContext(); - const spaceId = useKibanaSpaceId(); + const { space } = useActiveKibanaSpace(); + + // This is a rather crude way of guarding the dependent providers against + // arguments that are only made available asynchronously. Ideally, we'd use + // React concurrent mode and Suspense in order to handle that more gracefully. + if (sourceConfiguration?.configuration.logAlias == null || space == null) { + return null; + } return ( {children} diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 357f07265ac6e3..a1494a023201fc 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -4,16 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { +import type { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public'; +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { UsageCollectionSetup, UsageCollectionStart, } from '../../../../src/plugins/usage_collection/public'; -import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; -import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; -import { ObservabilityPluginSetup, ObservabilityPluginStart } from '../../observability/public'; +import type { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; +import type { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; +import type { + ObservabilityPluginSetup, + ObservabilityPluginStart, +} from '../../observability/public'; +import type { SpacesPluginStart } from '../../spaces/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -31,6 +35,7 @@ export interface InfraClientStartDeps { data: DataPublicPluginStart; dataEnhanced: DataEnhancedStart; observability: ObservabilityPluginStart; + spaces: SpacesPluginStart; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionStart; } diff --git a/x-pack/plugins/infra/public/utils/use_kibana_space_id.ts b/x-pack/plugins/infra/public/utils/use_kibana_space_id.ts deleted file mode 100644 index 86597f52928d59..00000000000000 --- a/x-pack/plugins/infra/public/utils/use_kibana_space_id.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as rt from 'io-ts'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; - -export const useKibanaSpaceId = (): string => { - const kibana = useKibana(); - // NOTE: The injectedMetadata service will be deprecated at some point. We should migrate - // this to the client side Spaces plugin when it becomes available. - const activeSpace = kibana.services.injectedMetadata?.getInjectedVar('activeSpace'); - - return pipe( - activeSpaceRT.decode(activeSpace), - fold( - () => 'default', - (decodedActiveSpace) => decodedActiveSpace.space.id - ) - ); -}; - -const activeSpaceRT = rt.type({ - space: rt.type({ - id: rt.string, - }), -}); From 8f98b72df85156179b7dc17bc77a99388562165b Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Aug 2020 09:19:59 -0400 Subject: [PATCH 04/31] [SECURITY] Bug investigate timeline (#74429) * simple solution to avoid duplicate request * fix investigate template timeline from template timeline --- .../containers/matrix_histogram/index.ts | 76 +++++---- .../public/common/mock/timeline_results.ts | 35 +++- .../components/alerts_table/actions.test.tsx | 12 +- .../components/alerts_table/actions.tsx | 47 ++++-- .../components/alerts_table/helpers.test.ts | 149 ++++++++++++------ .../components/alerts_table/helpers.ts | 35 ++-- .../components/alerts_table/index.tsx | 5 +- .../components/alerts_table/types.ts | 3 +- .../components/open_timeline/helpers.ts | 3 +- .../components/open_timeline/types.ts | 1 + 10 files changed, 255 insertions(+), 111 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 8c7acfc18ece68..2122eab23957a2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -7,6 +7,7 @@ import { isEmpty } from 'lodash/fp'; import { useEffect, useMemo, useState, useRef } from 'react'; +import { deepEqual } from 'hoek'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { errorToToaster, useStateToaster } from '../../components/toasters'; @@ -34,7 +35,6 @@ export const useQuery = ({ } return configIndex; }, [configIndex, indexToAdd]); - const [, dispatchToaster] = useStateToaster(); const refetch = useRef(); const [loading, setLoading] = useState(false); @@ -43,20 +43,54 @@ export const useQuery = ({ const [totalCount, setTotalCount] = useState(-1); const apolloClient = useApolloClient(); + const [matrixHistogramVariables, setMatrixHistogramVariables] = useState< + GetMatrixHistogramQuery.Variables + >({ + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }); + + useEffect(() => { + setMatrixHistogramVariables((prevVariables) => { + const localVariables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; + if (!deepEqual(prevVariables, localVariables)) { + return localVariables; + } + return prevVariables; + }); + }, [ + defaultIndex, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, + endDate, + ]); + useEffect(() => { - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, - }; let isSubscribed = true; const abortCtrl = new AbortController(); const abortSignal = abortCtrl.signal; @@ -102,19 +136,7 @@ export const useQuery = ({ isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - defaultIndex, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - data, - ]); + }, [apolloClient, dispatchToaster, errorMessage, matrixHistogramVariables]); return { data, loading, inspect, totalCount, refetch: refetch.current }; }; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index a415ab75f13ea5..ab9f12a67fe897 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -8,7 +8,13 @@ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_q import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; -import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types'; +import { + GetAllTimeline, + SortFieldTimeline, + TimelineResult, + Direction, + DetailItem, +} from '../../graphql/types'; import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; import { CreateTimelineProps } from '../../detections/components/alerts_table/types'; import { TimelineModel } from '../../timelines/store/timeline/model'; @@ -2252,5 +2258,32 @@ export const defaultTimelineProps: CreateTimelineProps = { width: 1100, }, to: '2018-11-05T19:03:25.937Z', + notes: null, ruleNote: '# this is some markdown documentation', }; + +export const mockTimelineDetails: DetailItem[] = [ + { + field: 'host.name', + values: ['apache'], + originalValue: 'apache', + }, + { + field: 'user.id', + values: ['1'], + originalValue: 1, + }, +]; + +export const mockTimelineDetailsApollo = { + data: { + source: { + TimelineDetails: { + data: mockTimelineDetails, + }, + }, + }, + loading: false, + networkStatus: 7, + stale: false, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index c2b51e29c230d4..e8015f601cb184 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { get } from 'lodash/fp'; import sinon from 'sinon'; import moment from 'moment'; @@ -12,6 +14,7 @@ import { defaultTimelineProps, apolloClient, mockTimelineApolloResult, + mockTimelineDetailsApollo, } from '../../../common/mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../graphql/types'; @@ -37,7 +40,13 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; - jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult); + jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { + const id = get('variables.id', obj); + if (id != null) { + return Promise.resolve(mockTimelineApolloResult); + } + return Promise.resolve(mockTimelineDetailsApollo); + }); clock = sinon.useFakeTimers(unix); }); @@ -71,6 +80,7 @@ describe('alert actions', () => { }); const expected = { from: '2018-11-05T18:58:25.937Z', + notes: null, timeline: { columns: [ { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 7bebc9efbee157..34c0537a6d7d24 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -19,6 +19,8 @@ import { Ecs, TimelineStatus, TimelineType, + GetTimelineDetailsQuery, + DetailItem, } from '../../../graphql/types'; import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -34,6 +36,7 @@ import { } from './helpers'; import { KueryFilterQueryKind } from '../../../common/store'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { timelineDetailsQuery } from '../../../timelines/containers/details/index.gql_query'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -153,35 +156,45 @@ export const sendAlertToTimelineAction = async ({ if (timelineId !== '' && apolloClient != null) { try { updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); - const responseTimeline = await apolloClient.query< - GetOneTimeline.Query, - GetOneTimeline.Variables - >({ - query: oneTimelineQuery, - fetchPolicy: 'no-cache', - variables: { - id: timelineId, - }, - }); + const [responseTimeline, eventDataResp] = await Promise.all([ + apolloClient.query({ + query: oneTimelineQuery, + fetchPolicy: 'no-cache', + variables: { + id: timelineId, + }, + }), + apolloClient.query({ + query: timelineDetailsQuery, + fetchPolicy: 'no-cache', + variables: { + defaultIndex: [], + docValueFields: [], + eventId: ecsData._id, + indexName: ecsData._index ?? '', + sourceId: 'default', + }, + }), + ]); const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); - + const eventData: DetailItem[] = getOr([], 'data.source.TimelineDetails.data', eventDataResp); if (!isEmpty(resultingTimeline)) { const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); openAlertInBasicTimeline = false; - const { timeline } = formatTimelineResultToModel( + const { timeline, notes } = formatTimelineResultToModel( timelineTemplate, true, timelineTemplate.timelineType ?? TimelineType.default ); const query = replaceTemplateFieldFromQuery( timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData, + eventData, timeline.timelineType ); - const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], eventData); const dataProviders = replaceTemplateFieldFromDataProviders( timeline.dataProviders ?? [], - ecsData, + eventData, timeline.timelineType ); @@ -213,10 +226,12 @@ export const sendAlertToTimelineAction = async ({ expression: query, }, }, + noteIds: notes?.map((n) => n.noteId) ?? [], show: true, }, to, ruleNote: noteContent, + notes: notes ?? null, }); } } catch { @@ -232,6 +247,7 @@ export const sendAlertToTimelineAction = async ({ ) { return createTimeline({ from, + notes: null, timeline: { ...timelineDefaults, dataProviders: [ @@ -282,6 +298,7 @@ export const sendAlertToTimelineAction = async ({ } else { return createTimeline({ from, + notes: null, timeline: { ...timelineDefaults, dataProviders: [ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts index 4decddd6b88860..7ac254f2e84f7f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash/fp'; import { TimelineType } from '../../../../common/types/timeline'; -import { mockEcsData } from '../../../common/mock/mock_ecs'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { DataProvider, @@ -20,31 +18,40 @@ import { replaceTemplateFieldFromMatchFilters, reformatDataProviderWithNewValue, } from './helpers'; +import { mockTimelineDetails } from '../../../common/mock'; describe('helpers', () => { - let mockEcsDataClone = cloneDeep(mockEcsData); - beforeEach(() => { - mockEcsDataClone = cloneDeep(mockEcsData); - }); describe('getStringOrStringArray', () => { test('it should correctly return a string array', () => { - const value = getStringArray('x', { - x: 'The nickname of the developer we all :heart:', - }); + const value = getStringArray('x', [ + { + field: 'x', + values: ['The nickname of the developer we all :heart:'], + originalValue: 'The nickname of the developer we all :heart:', + }, + ]); expect(value).toEqual(['The nickname of the developer we all :heart:']); }); test('it should correctly return a string array with a single element', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:'], - }); + const value = getStringArray('x', [ + { + field: 'x', + values: ['The nickname of the developer we all :heart:'], + originalValue: 'The nickname of the developer we all :heart:', + }, + ]); expect(value).toEqual(['The nickname of the developer we all :heart:']); }); test('it should correctly return a string array with two elements of strings', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], - }); + const value = getStringArray('x', [ + { + field: 'x', + values: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + originalValue: 'The nickname of the developer we all :heart:', + }, + ]); expect(value).toEqual([ 'The nickname of the developer we all :heart:', 'We are all made of stars', @@ -52,22 +59,40 @@ describe('helpers', () => { }); test('it should correctly return a string array with deep elements', () => { - const value = getStringArray('x.y.z', { - x: { y: { z: 'zed' } }, - }); + const value = getStringArray('x.y.z', [ + { + field: 'x.y.z', + values: ['zed'], + originalValue: 'zed', + }, + ]); expect(value).toEqual(['zed']); }); test('it should correctly return a string array with a non-existent value', () => { - const value = getStringArray('non.existent', { - x: { y: { z: 'zed' } }, - }); + const value = getStringArray('non.existent', [ + { + field: 'x.y.z', + values: ['zed'], + originalValue: 'zed', + }, + ]); expect(value).toEqual([]); }); test('it should trace an error if the value is not a string', () => { const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: 5 }, mockConsole); + const value = getStringArray( + 'a', + [ + { + field: 'a', + values: (5 as unknown) as string[], + originalValue: 'zed', + }, + ], + mockConsole + ); expect(value).toEqual([]); expect( mockConsole.trace @@ -77,13 +102,23 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - { a: 5 } + [{ field: 'a', originalValue: 'zed', values: 5 }] ); }); test('it should trace an error if the value is an array of mixed values', () => { const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + const value = getStringArray( + 'a', + [ + { + field: 'a', + values: (['hi', 5] as unknown) as string[], + originalValue: 'zed', + }, + ], + mockConsole + ); expect(value).toEqual([]); expect( mockConsole.trace @@ -93,7 +128,7 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - { a: ['hi', 5] } + [{ field: 'a', originalValue: 'zed', values: ['hi', 5] }] ); }); }); @@ -103,7 +138,7 @@ describe('helpers', () => { test('given an empty query string this returns an empty query string', () => { const replacement = replaceTemplateFieldFromQuery( '', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual(''); @@ -112,7 +147,7 @@ describe('helpers', () => { test('given a query string with spaces this returns an empty query string', () => { const replacement = replaceTemplateFieldFromQuery( ' ', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual(''); @@ -121,17 +156,21 @@ describe('helpers', () => { test('it should replace a query with a template value such as apache from a mock template', () => { const replacement = replaceTemplateFieldFromQuery( 'host.name: placeholdertext', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual('host.name: apache'); }); test('it should replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const dupTimelineDetails = [...mockTimelineDetails]; + dupTimelineDetails[0] = { + ...dupTimelineDetails[0], + values: ('apache' as unknown) as string[], + }; // very unsafe cast for this test case const replacement = replaceTemplateFieldFromQuery( 'host.name: *', - mockEcsDataClone[0], + dupTimelineDetails, TimelineType.default ); expect(replacement).toEqual('host.name: *'); @@ -140,7 +179,7 @@ describe('helpers', () => { test('it should NOT replace a query with a template value that is not part of the template fields array', () => { const replacement = replaceTemplateFieldFromQuery( 'user.id: placeholdertext', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual('user.id: placeholdertext'); @@ -151,7 +190,7 @@ describe('helpers', () => { test('given an empty query string this returns an empty query string', () => { const replacement = replaceTemplateFieldFromQuery( '', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual(''); @@ -160,7 +199,7 @@ describe('helpers', () => { test('given a query string with spaces this returns an empty query string', () => { const replacement = replaceTemplateFieldFromQuery( ' ', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual(''); @@ -169,17 +208,21 @@ describe('helpers', () => { test('it should NOT replace a query with a template value such as apache from a mock template', () => { const replacement = replaceTemplateFieldFromQuery( 'host.name: placeholdertext', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual('host.name: placeholdertext'); }); test('it should NOT replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const dupTimelineDetails = [...mockTimelineDetails]; + dupTimelineDetails[0] = { + ...dupTimelineDetails[0], + values: ('apache' as unknown) as string[], + }; // very unsafe cast for this test case const replacement = replaceTemplateFieldFromQuery( 'host.name: *', - mockEcsDataClone[0], + dupTimelineDetails, TimelineType.default ); expect(replacement).toEqual('host.name: *'); @@ -188,7 +231,7 @@ describe('helpers', () => { test('it should NOT replace a query with a template value that is not part of the template fields array', () => { const replacement = replaceTemplateFieldFromQuery( 'user.id: placeholdertext', - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual('user.id: placeholdertext'); @@ -198,7 +241,7 @@ describe('helpers', () => { describe('replaceTemplateFieldFromMatchFilters', () => { test('given an empty query filter this will return an empty filter', () => { - const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + const replacement = replaceTemplateFieldFromMatchFilters([], mockTimelineDetails); expect(replacement).toEqual([]); }); @@ -216,7 +259,7 @@ describe('helpers', () => { query: { match_phrase: { 'host.name': 'Braden' } }, }, ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockTimelineDetails); const expected: Filter[] = [ { meta: { @@ -247,7 +290,7 @@ describe('helpers', () => { query: { match_phrase: { 'user.id': 'Evan' } }, }, ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockTimelineDetails); const expected: Filter[] = [ { meta: { @@ -275,7 +318,7 @@ describe('helpers', () => { mockDataProvider.queryMatch.value = 'Braden'; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual({ @@ -297,7 +340,11 @@ describe('helpers', () => { }); test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const dupTimelineDetails = [...mockTimelineDetails]; + dupTimelineDetails[0] = { + ...dupTimelineDetails[0], + values: ('apache' as unknown) as string[], + }; // very unsafe cast for this test case const mockDataProvider: DataProvider = mockDataProviders[0]; mockDataProvider.queryMatch.field = 'host.name'; mockDataProvider.id = 'Braden'; @@ -305,7 +352,7 @@ describe('helpers', () => { mockDataProvider.queryMatch.value = 'Braden'; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + dupTimelineDetails, TimelineType.default ); expect(replacement).toEqual({ @@ -334,7 +381,7 @@ describe('helpers', () => { mockDataProvider.queryMatch.value = 'Rebecca'; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.default ); expect(replacement).toEqual({ @@ -366,7 +413,7 @@ describe('helpers', () => { mockDataProvider.type = DataProviderType.template; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual({ @@ -396,7 +443,7 @@ describe('helpers', () => { mockDataProvider.type = DataProviderType.default; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual({ @@ -418,7 +465,11 @@ describe('helpers', () => { }); test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const dupTimelineDetails = [...mockTimelineDetails]; + dupTimelineDetails[0] = { + ...dupTimelineDetails[0], + values: ('apache' as unknown) as string[], + }; // very unsafe cast for this test case const mockDataProvider: DataProvider = mockDataProviders[0]; mockDataProvider.queryMatch.field = 'host.name'; mockDataProvider.id = 'Braden'; @@ -427,7 +478,7 @@ describe('helpers', () => { mockDataProvider.type = DataProviderType.template; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + dupTimelineDetails, TimelineType.template ); expect(replacement).toEqual({ @@ -457,7 +508,7 @@ describe('helpers', () => { mockDataProvider.type = DataProviderType.default; const replacement = reformatDataProviderWithNewValue( mockDataProvider, - mockEcsDataClone[0], + mockTimelineDetails, TimelineType.template ); expect(replacement).toEqual({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts index 084e4bff7e0ac0..20c233a03a8cfa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public'; import { DataProvider, DataProviderType, DataProvidersAnd, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Ecs, TimelineType } from '../../../graphql/types'; +import { DetailItem, TimelineType } from '../../../graphql/types'; interface FindValueToChangeInQuery { field: string; @@ -47,8 +47,12 @@ const templateFields = [ * @param data The unknown data that is typically a ECS value to get the value * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console */ -export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { - const value: unknown | undefined = get(field, data); +export const getStringArray = ( + field: string, + data: DetailItem[], + localConsole = console +): string[] => { + const value: unknown | undefined = data.find((d) => d.field === field)?.values ?? null; if (value == null) { return []; } else if (typeof value === 'string') { @@ -104,14 +108,14 @@ export const findValueToChangeInQuery = ( export const replaceTemplateFieldFromQuery = ( query: string, - ecsData: Ecs, + eventData: DetailItem[], timelineType: TimelineType = TimelineType.default ): string => { if (timelineType === TimelineType.default) { if (query.trim() !== '') { const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); return valueToChange.reduce((newQuery, vtc) => { - const newValue = getStringArray(vtc.field, ecsData); + const newValue = getStringArray(vtc.field, eventData); if (newValue.length) { return newQuery.replace(vtc.valueToChange, newValue[0]); } else { @@ -126,14 +130,17 @@ export const replaceTemplateFieldFromQuery = ( return query.trim(); }; -export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => +export const replaceTemplateFieldFromMatchFilters = ( + filters: Filter[], + eventData: DetailItem[] +): Filter[] => filters.map((filter) => { if ( filter.meta.type === 'phrase' && filter.meta.key != null && templateFields.includes(filter.meta.key) ) { - const newValue = getStringArray(filter.meta.key, ecsData); + const newValue = getStringArray(filter.meta.key, eventData); if (newValue.length) { filter.meta.params = { query: newValue[0] }; filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; @@ -144,13 +151,13 @@ export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: export const reformatDataProviderWithNewValue = ( dataProvider: T, - ecsData: Ecs, + eventData: DetailItem[], timelineType: TimelineType = TimelineType.default ): T => { // Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields if (timelineType !== TimelineType.template) { if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + const newValue = getStringArray(dataProvider.queryMatch.field, eventData); if (newValue.length) { dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); dataProvider.name = newValue[0]; @@ -168,7 +175,7 @@ export const reformatDataProviderWithNewValue = dataProviders.map((dataProvider) => { - const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData, timelineType); + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, eventData, timelineType); if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { newDataProvider.and = newDataProvider.and.map((andDataProvider) => - reformatDataProviderWithNewValue(andDataProvider, ecsData, timelineType) + reformatDataProviderWithNewValue(andDataProvider, eventData, timelineType) ); } return newDataProvider; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index d93bad29f33487..66423259ec1556 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -147,13 +147,14 @@ export const AlertsTableComponent: React.FC = ({ // Callback for creating a new timeline -- utilized by row/batch actions const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote, notes }: CreateTimelineProps) => { updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); updateTimeline({ duplicate: true, + forceNotes: true, from: fromTimeline, id: 'timeline-1', - notes: [], + notes, timeline: { ...timeline, show: true, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index ebf1a6d3ed533e..2e77e77f6b3d50 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -7,7 +7,7 @@ import ApolloClient from 'apollo-client'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; +import { Ecs, NoteResult, TimelineNonEcsData } from '../../../graphql/types'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { inputsModel } from '../../../common/store'; @@ -63,6 +63,7 @@ export interface CreateTimelineProps { from: string; timeline: TimelineModel; to: string; + notes: NoteResult[] | null; ruleNote?: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 0a08e45324b890..c2e23cc19d89e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -363,6 +363,7 @@ export const queryTimelineById = ({ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeline => ({ duplicate, id, + forceNotes = false, from, notes, timeline, @@ -407,7 +408,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); } - if (!duplicate) { + if (!duplicate || forceNotes) { dispatch( dispatchAddNotes({ notes: diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 8950f814d6965d..769a0a1658a469 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -192,6 +192,7 @@ export interface OpenTimelineProps { export interface UpdateTimeline { duplicate: boolean; id: string; + forceNotes?: boolean; from: string; notes: NoteResult[] | null | undefined; timeline: TimelineModel; From d43d45d77a3cfba62c5f28b2906d33e3fc3eee01 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 6 Aug 2020 09:26:00 -0400 Subject: [PATCH 05/31] [Resolver] Safer types (#74366) All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error. Specific and nuanced handling of these cases isn't the goal of this PR. This PR just introduces some helper types that can be used to assist you in writing defensive code. When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`. ### Added New Resolver specific types `ResolverEvent` has a new safe equivalent `SafeResolverEvent`. The constituent parts of `ResolverEvent` also have safe equivalents: `SafeEndpointEvent` and `SafeLegacyEndpointEvent`. Use these in your code for added type safety. ### New safe event methods The event methods accept the unsafe `ResolverEvent`. Create new methods that accept the safe `SafeResolverEvent`. By keeping copies of both methods around we can gradually transition to the safe versions: * `isLegacyEvent` has `isLegacyEventSafeVersion` * `eventTimestamp` has `timestampSafeVersion` * `eventName` has `processNameSafeVersion` * `eventId` has `eventIDSafeVersion` * `entityId` has `entityIDSafeVersion` * `parentEntityId` has `parentEntityIDSafeVersion` --- .../endpoint/models/ecs_safety_helpers.ts | 61 +++ .../common/endpoint/models/event.ts | 57 ++- .../common/endpoint/types.ts | 126 ++++++ .../isometric_taxi_layout.test.ts.snap | 30 +- .../models/indexed_process_tree/index.ts | 49 ++- .../isometric_taxi_layout.ts | 68 +-- .../resolver/models/process_event.test.ts | 24 +- .../public/resolver/models/process_event.ts | 12 +- .../public/resolver/store/actions.ts | 4 +- .../public/resolver/store/data/selectors.ts | 21 +- .../public/resolver/store/methods.ts | 4 +- .../public/resolver/store/selectors.test.ts | 17 +- .../public/resolver/store/selectors.ts | 13 +- .../public/resolver/types.ts | 20 +- .../public/resolver/view/map.tsx | 4 +- .../panels/panel_content_process_list.tsx | 402 +++++++++--------- .../resolver/view/process_event_dot.tsx | 19 +- .../view/resolver_without_providers.tsx | 4 +- .../public/resolver/view/use_camera.test.tsx | 2 +- 19 files changed, 616 insertions(+), 321 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts new file mode 100644 index 00000000000000..8b419e90a6ee9c --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ECSField } from '../types'; + +/** + * Use these functions to accecss information held in `ECSField`s. + */ + +/** + * True if the field contains `expected`. If the field contains an array, this will be true if the array contains `expected`. + */ +export function hasValue(valueOrCollection: ECSField, expected: T): boolean { + if (Array.isArray(valueOrCollection)) { + return valueOrCollection.includes(expected); + } else { + return valueOrCollection === expected; + } +} + +/** + * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null. + */ +export function firstNonNullValue(valueOrCollection: ECSField): T | undefined { + if (valueOrCollection === null) { + return undefined; + } else if (Array.isArray(valueOrCollection)) { + for (const value of valueOrCollection) { + if (value !== null) { + return value; + } + } + } else { + return valueOrCollection; + } +} + +/* + * Get an array of all non-null values. If there is just 1 value, return it wrapped in an array. If there are multiple values, return the non-null ones. + * Use this when you want to consistently access the value(s) as an array. + */ +export function values(valueOrCollection: ECSField): T[] { + if (Array.isArray(valueOrCollection)) { + const nonNullValues: T[] = []; + for (const value of valueOrCollection) { + if (value !== null) { + nonNullValues.push(value); + } + } + return nonNullValues; + } else if (valueOrCollection !== null) { + // if there is a single non-null value, wrap it in an array and return it. + return [valueOrCollection]; + } else { + // if the value was null, return `[]`. + return []; + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 1168b5edb6ffd7..b1a8524a9f9e79 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -3,8 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent, ResolverEvent } from '../types'; +import { + LegacyEndpointEvent, + ResolverEvent, + SafeResolverEvent, + SafeLegacyEndpointEvent, +} from '../types'; +import { firstNonNullValue } from './ecs_safety_helpers'; +/* + * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`. + */ +export function isLegacyEventSafeVersion( + event: SafeResolverEvent +): event is SafeLegacyEndpointEvent { + return 'endgame' in event && event.endgame !== undefined; +} + +/* + * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`. See `isLegacyEventSafeVersion` + */ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent { return (event as LegacyEndpointEvent).endgame !== undefined; } @@ -31,6 +49,12 @@ export function isProcessRunning(event: ResolverEvent): boolean { ); } +export function timestampSafeVersion(event: SafeResolverEvent): string | undefined | number { + return isLegacyEventSafeVersion(event) + ? firstNonNullValue(event.endgame?.timestamp_utc) + : firstNonNullValue(event?.['@timestamp']); +} + export function eventTimestamp(event: ResolverEvent): string | undefined | number { if (isLegacyEvent(event)) { return event.endgame.timestamp_utc; @@ -47,6 +71,14 @@ export function eventName(event: ResolverEvent): string { } } +export function processNameSafeVersion(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + return firstNonNullValue(event.endgame.process_name); + } else { + return firstNonNullValue(event.process?.name); + } +} + export function eventId(event: ResolverEvent): number | undefined | string { if (isLegacyEvent(event)) { return event.endgame.serial_event_id; @@ -54,6 +86,12 @@ export function eventId(event: ResolverEvent): number | undefined | string { return event.event.id; } +export function eventIDSafeVersion(event: SafeResolverEvent): number | undefined | string { + return firstNonNullValue( + isLegacyEventSafeVersion(event) ? event.endgame?.serial_event_id : event.event?.id + ); +} + export function entityId(event: ResolverEvent): string { if (isLegacyEvent(event)) { return event.endgame.unique_pid ? String(event.endgame.unique_pid) : ''; @@ -61,6 +99,16 @@ export function entityId(event: ResolverEvent): string { return event.process.entity_id; } +export function entityIDSafeVersion(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + return event.endgame?.unique_pid === undefined + ? undefined + : String(firstNonNullValue(event.endgame.unique_pid)); + } else { + return firstNonNullValue(event.process?.entity_id); + } +} + export function parentEntityId(event: ResolverEvent): string | undefined { if (isLegacyEvent(event)) { return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined; @@ -68,6 +116,13 @@ export function parentEntityId(event: ResolverEvent): string | undefined { return event.process.parent?.entity_id; } +export function parentEntityIDSafeVersion(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + return String(firstNonNullValue(event.endgame.unique_ppid)); + } + return firstNonNullValue(event.process?.parent?.entity_id); +} + export function ancestryArray(event: ResolverEvent): string[] | undefined { if (isLegacyEvent(event)) { return undefined; diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 1c24e1abe5a57e..61ce672405fd58 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -508,6 +508,8 @@ export interface EndpointEvent { ecs: { version: string; }; + // A legacy has `endgame` and an `EndpointEvent` (AKA ECS event) will never have it. This helps TS narrow `SafeResolverEvent`. + endgame?: never; event: { category: string | string[]; type: string | string[]; @@ -559,6 +561,130 @@ export interface EndpointEvent { export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; +/** + * All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error. + * When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`. + */ +export type ECSField = T | null | Array; + +/** + * A more conservative version of `ResolverEvent` that treats fields as optional and use `ECSField` to type all ECS fields. + * Prefer this over `ResolverEvent`. + */ +export type SafeResolverEvent = SafeEndpointEvent | SafeLegacyEndpointEvent; + +/** + * Safer version of ResolverEvent. Please use this going forward. + */ +export type SafeEndpointEvent = Partial<{ + '@timestamp': ECSField; + agent: Partial<{ + id: ECSField; + version: ECSField; + type: ECSField; + }>; + ecs: Partial<{ + version: ECSField; + }>; + event: Partial<{ + category: ECSField; + type: ECSField; + id: ECSField; + kind: ECSField; + }>; + host: Partial<{ + id: ECSField; + hostname: ECSField; + name: ECSField; + ip: ECSField; + mac: ECSField; + architecture: ECSField; + os: Partial<{ + full: ECSField; + name: ECSField; + version: ECSField; + platform: ECSField; + family: ECSField; + Ext: Partial<{ + variant: ECSField; + }>; + }>; + }>; + network: Partial<{ + direction: ECSField; + forwarded_ip: ECSField; + }>; + dns: Partial<{ + question: Partial<{ name: ECSField }>; + }>; + process: Partial<{ + entity_id: ECSField; + name: ECSField; + executable: ECSField; + args: ECSField; + code_signature: Partial<{ + status: ECSField; + subject_name: ECSField; + }>; + pid: ECSField; + hash: Partial<{ + md5: ECSField; + }>; + parent: Partial<{ + entity_id: ECSField; + name: ECSField; + pid: ECSField; + }>; + /* + * The array has a special format. The entity_ids towards the beginning of the array are closer ancestors and the + * values towards the end of the array are more distant ancestors (grandparents). Therefore + * ancestry_array[0] == process.parent.entity_id and ancestry_array[1] == process.parent.parent.entity_id + */ + Ext: Partial<{ + ancestry: ECSField; + }>; + }>; + user: Partial<{ + domain: ECSField; + name: ECSField; + }>; + file: Partial<{ path: ECSField }>; + registry: Partial<{ path: ECSField; key: ECSField }>; +}>; + +export interface SafeLegacyEndpointEvent { + '@timestamp'?: ECSField; + /** + * 'legacy' events must have an `endgame` key. + */ + endgame: Partial<{ + pid: ECSField; + ppid: ECSField; + event_type_full: ECSField; + event_subtype_full: ECSField; + event_timestamp: ECSField; + event_type: ECSField; + unique_pid: ECSField; + unique_ppid: ECSField; + machine_id: ECSField; + process_name: ECSField; + process_path: ECSField; + timestamp_utc: ECSField; + serial_event_id: ECSField; + }>; + agent: Partial<{ + id: ECSField; + type: ECSField; + version: ECSField; + }>; + event: Partial<{ + action: ECSField; + type: ECSField; + category: ECSField; + id: ECSField; + }>; +} + /** * The response body for the resolver '/entity' index API */ diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index 6f26bfe063c055..db8d047c2ce860 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -182,7 +182,7 @@ Object { "edgeLineSegments": Array [ Object { "metadata": Object { - "uniqueId": "parentToMid", + "uniqueId": "parentToMidedge:0:1", }, "points": Array [ Array [ @@ -197,7 +197,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midway", + "uniqueId": "midwayedge:0:1", }, "points": Array [ Array [ @@ -212,7 +212,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "", + "uniqueId": "edge:0:1", }, "points": Array [ Array [ @@ -227,7 +227,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "", + "uniqueId": "edge:0:2", }, "points": Array [ Array [ @@ -242,7 +242,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "", + "uniqueId": "edge:0:8", }, "points": Array [ Array [ @@ -257,7 +257,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "parentToMid13", + "uniqueId": "parentToMidedge:1:3", }, "points": Array [ Array [ @@ -272,7 +272,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midway13", + "uniqueId": "midwayedge:1:3", }, "points": Array [ Array [ @@ -287,7 +287,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "13", + "uniqueId": "edge:1:3", }, "points": Array [ Array [ @@ -302,7 +302,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "14", + "uniqueId": "edge:1:4", }, "points": Array [ Array [ @@ -317,7 +317,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "parentToMid25", + "uniqueId": "parentToMidedge:2:5", }, "points": Array [ Array [ @@ -332,7 +332,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midway25", + "uniqueId": "midwayedge:2:5", }, "points": Array [ Array [ @@ -347,7 +347,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "25", + "uniqueId": "edge:2:5", }, "points": Array [ Array [ @@ -362,7 +362,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "26", + "uniqueId": "edge:2:6", }, "points": Array [ Array [ @@ -377,7 +377,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "67", + "uniqueId": "edge:6:7", }, "points": Array [ Array [ @@ -584,7 +584,7 @@ Object { "edgeLineSegments": Array [ Object { "metadata": Object { - "uniqueId": "", + "uniqueId": "edge:0:1", }, "points": Array [ Array [ diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts index 060a014b8730f2..f6b893ba25b78d 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uniquePidForProcess, uniqueParentPidForProcess, orderByTime } from '../process_event'; +import { orderByTime } from '../process_event'; import { IndexedProcessTree } from '../../types'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers'; +import * as eventModel from '../../../../common/endpoint/models/event'; /** * Create a new IndexedProcessTree from an array of ProcessEvents. @@ -15,24 +16,25 @@ import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers'; */ export function factory( // Array of processes to index as a tree - processes: ResolverEvent[] + processes: SafeResolverEvent[] ): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { - const uniqueProcessPid = uniquePidForProcess(process); - idToValue.set(uniqueProcessPid, process); + const entityID: string | undefined = eventModel.entityIDSafeVersion(process); + if (entityID !== undefined) { + idToValue.set(entityID, process); - // NB: If the value was null or undefined, use `undefined` - const uniqueParentPid: string | undefined = uniqueParentPidForProcess(process) ?? undefined; + const uniqueParentPid: string | undefined = eventModel.parentEntityIDSafeVersion(process); - let childrenWithTheSameParent = idToChildren.get(uniqueParentPid); - if (!childrenWithTheSameParent) { - childrenWithTheSameParent = []; - idToChildren.set(uniqueParentPid, childrenWithTheSameParent); + let childrenWithTheSameParent = idToChildren.get(uniqueParentPid); + if (!childrenWithTheSameParent) { + childrenWithTheSameParent = []; + idToChildren.set(uniqueParentPid, childrenWithTheSameParent); + } + childrenWithTheSameParent.push(process); } - childrenWithTheSameParent.push(process); } // sort the children of each node @@ -49,7 +51,10 @@ export function factory( /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children(tree: IndexedProcessTree, parentID: string | undefined): ResolverEvent[] { +export function children( + tree: IndexedProcessTree, + parentID: string | undefined +): SafeResolverEvent[] { const currentProcessSiblings = tree.idToChildren.get(parentID); return currentProcessSiblings === undefined ? [] : currentProcessSiblings; } @@ -57,7 +62,7 @@ export function children(tree: IndexedProcessTree, parentID: string | undefined) /** * Get the indexed process event for the ID */ -export function processEvent(tree: IndexedProcessTree, entityID: string): ResolverEvent | null { +export function processEvent(tree: IndexedProcessTree, entityID: string): SafeResolverEvent | null { return tree.idToProcess.get(entityID) ?? null; } @@ -66,9 +71,9 @@ export function processEvent(tree: IndexedProcessTree, entityID: string): Resolv */ export function parent( tree: IndexedProcessTree, - childProcess: ResolverEvent -): ResolverEvent | undefined { - const uniqueParentPid = uniqueParentPidForProcess(childProcess); + childProcess: SafeResolverEvent +): SafeResolverEvent | undefined { + const uniqueParentPid = eventModel.parentEntityIDSafeVersion(childProcess); if (uniqueParentPid === undefined) { return undefined; } else { @@ -91,7 +96,7 @@ export function root(tree: IndexedProcessTree) { return null; } // any node will do - let current: ResolverEvent = tree.idToProcess.values().next().value; + let current: SafeResolverEvent = tree.idToProcess.values().next().value; // iteratively swap current w/ its parent while (parent(tree, current) !== undefined) { @@ -106,8 +111,8 @@ export function root(tree: IndexedProcessTree) { export function* levelOrder(tree: IndexedProcessTree) { const rootNode = root(tree); if (rootNode !== null) { - yield* baseLevelOrder(rootNode, (parentNode: ResolverEvent): ResolverEvent[] => - children(tree, uniquePidForProcess(parentNode)) + yield* baseLevelOrder(rootNode, (parentNode: SafeResolverEvent): SafeResolverEvent[] => + children(tree, eventModel.entityIDSafeVersion(parentNode)) ); } } diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index 1fc2ea0150aee6..f0880fa635a24c 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -14,12 +14,11 @@ import { Matrix3, IsometricTaxiLayout, } from '../../types'; -import * as event from '../../../../common/endpoint/models/event'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import * as vector2 from '../vector2'; import * as indexedProcessTreeModel from './index'; import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date'; -import { uniquePidForProcess } from '../process_event'; /** * Graph the process tree @@ -30,25 +29,29 @@ export function isometricTaxiLayoutFactory( /** * Walk the tree in reverse level order, calculating the 'width' of subtrees. */ - const widths = widthsOfProcessSubtrees(indexedProcessTree); + const widths: Map = widthsOfProcessSubtrees(indexedProcessTree); /** * Walk the tree in level order. Using the precalculated widths, calculate the position of nodes. * Nodes are positioned relative to their parents and preceding siblings. */ - const positions = processPositions(indexedProcessTree, widths); + const positions: Map = processPositions(indexedProcessTree, widths); /** * With the widths and positions precalculated, we calculate edge line segments (arrays of vector2s) * which connect them in a 'pitchfork' design. */ - const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions); + const edgeLineSegments: EdgeLineSegment[] = processEdgeLineSegments( + indexedProcessTree, + widths, + positions + ); /** * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set( @@ -83,8 +86,8 @@ export function isometricTaxiLayoutFactory( /** * Calculate a level (starting at 1) for each node. */ -function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { - const map: Map = new Map(); +function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { + const map: Map = new Map(); for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) { const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node); if (parentNode === undefined) { @@ -143,20 +146,20 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map(); + const widths = new Map(); if (indexedProcessTreeModel.size(indexedProcessTree) === 0) { return widths; } - const processesInReverseLevelOrder: ResolverEvent[] = [ + const processesInReverseLevelOrder: SafeResolverEvent[] = [ ...indexedProcessTreeModel.levelOrder(indexedProcessTree), ].reverse(); for (const process of processesInReverseLevelOrder) { const children = indexedProcessTreeModel.children( indexedProcessTree, - uniquePidForProcess(process) + eventModel.entityIDSafeVersion(process) ); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { @@ -167,7 +170,7 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces * Therefore a parent can always find a width for its children, since all of its children * will have been handled already. */ - return currentValue + widths.get(child)!; + return currentValue + (widths.get(child) ?? 0); }, 0); }; @@ -178,6 +181,9 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces return widths; } +/** + * Layout the graph. Note: if any process events are missing the `entity_id`, this will throw an Error. + */ function processEdgeLineSegments( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths, @@ -196,9 +202,13 @@ function processEdgeLineSegments( const { process, parent, parentWidth } = metadata; const position = positions.get(process); const parentPosition = positions.get(parent); - const parentId = event.entityId(parent); - const processEntityId = event.entityId(process); - const edgeLineId = parentId ? parentId + processEntityId : parentId; + const parentID = eventModel.entityIDSafeVersion(parent); + const processEntityID = eventModel.entityIDSafeVersion(process); + + if (processEntityID === undefined) { + throw new Error('tried to graph a Resolver that had a process with no `process.entity_id`'); + } + const edgeLineID = `edge:${parentID ?? 'undefined'}:${processEntityID}`; if (position === undefined || parentPosition === undefined) { /** @@ -207,12 +217,12 @@ function processEdgeLineSegments( throw new Error(); } - const parentTime = event.eventTimestamp(parent); - const processTime = event.eventTimestamp(process); + const parentTime = eventModel.timestampSafeVersion(parent); + const processTime = eventModel.timestampSafeVersion(process); if (parentTime && processTime) { edgeLineMetadata.elapsedTime = elapsedTime(parentTime, processTime) ?? undefined; } - edgeLineMetadata.uniqueId = edgeLineId; + edgeLineMetadata.uniqueId = edgeLineID; /** * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line @@ -236,7 +246,7 @@ function processEdgeLineSegments( const siblings = indexedProcessTreeModel.children( indexedProcessTree, - uniquePidForProcess(parent) + eventModel.entityIDSafeVersion(parent) ); const isFirstChild = process === siblings[0]; @@ -260,7 +270,7 @@ function processEdgeLineSegments( const lineFromParentToMidwayLine: EdgeLineSegment = { points: [parentPosition, [parentPosition[0], midwayY]], - metadata: { uniqueId: `parentToMid${edgeLineId}` }, + metadata: { uniqueId: `parentToMid${edgeLineID}` }, }; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; @@ -281,7 +291,7 @@ function processEdgeLineSegments( midwayY, ], ], - metadata: { uniqueId: `midway${edgeLineId}` }, + metadata: { uniqueId: `midway${edgeLineID}` }, }; edgeLineSegments.push( @@ -303,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: ResolverEvent | undefined; + let lastProcessedParentNode: SafeResolverEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -431,7 +441,10 @@ function* levelOrderWithWidths( parentWidth, }; - const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent)); + const siblings = indexedProcessTreeModel.children( + tree, + eventModel.entityIDSafeVersion(parent) + ); if (siblings.length === 1) { metadata.isOnlyChild = true; metadata.lastChildWidth = width; @@ -488,7 +501,10 @@ const distanceBetweenNodesInUnits = 2; */ const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined { +export function nodePosition( + model: IsometricTaxiLayout, + node: SafeResolverEvent +): Vector2 | undefined { return model.processNodePositions.get(node); } diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts index 4b1d555d0a7c38..4d48b34fb2841b 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts @@ -6,7 +6,11 @@ import { eventType, orderByTime, userInfoForProcess } from './process_event'; import { mockProcessEvent } from './process_event_test_helpers'; -import { LegacyEndpointEvent, ResolverEvent } from '../../../common/endpoint/types'; +import { + LegacyEndpointEvent, + ResolverEvent, + SafeResolverEvent, +} from '../../../common/endpoint/types'; describe('process event', () => { describe('eventType', () => { @@ -42,7 +46,7 @@ describe('process event', () => { }); describe('orderByTime', () => { let mock: (time: number, eventID: string) => ResolverEvent; - let events: ResolverEvent[]; + let events: SafeResolverEvent[]; beforeEach(() => { mock = (time, eventID) => { return { @@ -56,14 +60,14 @@ describe('process event', () => { // each event has a unique id, a through h // order is arbitrary events = [ - mock(-1, 'a'), - mock(0, 'c'), - mock(1, 'e'), - mock(NaN, 'g'), - mock(-1, 'b'), - mock(0, 'd'), - mock(1, 'f'), - mock(NaN, 'h'), + mock(-1, 'a') as SafeResolverEvent, + mock(0, 'c') as SafeResolverEvent, + mock(1, 'e') as SafeResolverEvent, + mock(NaN, 'g') as SafeResolverEvent, + mock(-1, 'b') as SafeResolverEvent, + mock(0, 'd') as SafeResolverEvent, + mock(1, 'f') as SafeResolverEvent, + mock(NaN, 'h') as SafeResolverEvent, ]; }); it('sorts events as expected', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 1a5c67f6a6f2ff..ea588731a55c82 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -5,7 +5,7 @@ */ import * as event from '../../../common/endpoint/models/event'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { ResolverProcessType } from '../types'; /** @@ -32,8 +32,8 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) { * ms since Unix epoc, based on timestamp. * may return NaN if the timestamp wasn't present or was invalid. */ -export function datetime(passedEvent: ResolverEvent): number | null { - const timestamp = event.eventTimestamp(passedEvent); +export function datetime(passedEvent: SafeResolverEvent): number | null { + const timestamp = event.timestampSafeVersion(passedEvent); const time = timestamp === undefined ? 0 : new Date(timestamp).getTime(); @@ -178,13 +178,15 @@ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { /** * used to sort events */ -export function orderByTime(first: ResolverEvent, second: ResolverEvent): number { +export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent): number { const firstDatetime: number | null = datetime(first); const secondDatetime: number | null = datetime(second); if (firstDatetime === secondDatetime) { // break ties using an arbitrary (stable) comparison of `eventId` (which should be unique) - return String(event.eventId(first)).localeCompare(String(event.eventId(second))); + return String(event.eventIDSafeVersion(first)).localeCompare( + String(event.eventIDSafeVersion(second)) + ); } else if (firstDatetime === null || secondDatetime === null) { // sort `null`'s as higher than numbers return (firstDatetime === null ? 1 : 0) - (secondDatetime === null ? 1 : 0); diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 418eb0d837276f..29c03215e9ff4f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CameraAction } from './camera'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { DataAction } from './data/action'; /** @@ -96,7 +96,7 @@ interface UserSelectedResolverNode { interface UserSelectedRelatedEventCategory { readonly type: 'userSelectedRelatedEventCategory'; readonly payload: { - subject: ResolverEvent; + subject: SafeResolverEvent; category?: string; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 272d0aae7eef47..569a24bb8537e3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -28,10 +28,11 @@ import { ResolverTree, ResolverNodeStats, ResolverRelatedEvents, + SafeResolverEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; -import { allEventCategories } from '../../../../common/endpoint/models/event'; +import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; /** @@ -145,7 +146,7 @@ export const tree = createSelector(graphableProcesses, function indexedTree( graphableProcesses /* eslint-enable no-shadow */ ) { - return indexedProcessTreeModel.factory(graphableProcesses); + return indexedProcessTreeModel.factory(graphableProcesses as SafeResolverEvent[]); }); /** @@ -194,7 +195,9 @@ export const relatedEventsByCategory: ( } return relatedById.events.reduce( (eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => { - if ([candidate && allEventCategories(candidate)].flat().includes(ecsCategory)) { + if ( + [candidate && eventModel.allEventCategories(candidate)].flat().includes(ecsCategory) + ) { eventsByCategory.push(candidate); } return eventsByCategory; @@ -280,7 +283,7 @@ export const relatedEventInfoByEntityId: ( return []; } return eventsResponseForThisEntry.events.filter((resolverEvent) => { - for (const category of [allEventCategories(resolverEvent)].flat()) { + for (const category of [eventModel.allEventCategories(resolverEvent)].flat()) { if (category === eventCategory) { return true; } @@ -404,7 +407,7 @@ export const processEventForID: ( ) => (nodeID: string) => ResolverEvent | null = createSelector( tree, (indexedProcessTree) => (nodeID: string) => - indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) + indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) as ResolverEvent ); /** @@ -415,7 +418,7 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null processEventForID, ({ ariaLevels }, processEventGetter) => (nodeID: string) => { const node = processEventGetter(nodeID); - return node ? ariaLevels.get(node) ?? null : null; + return node ? ariaLevels.get(node as SafeResolverEvent) ?? null : null; } ); @@ -468,10 +471,10 @@ export const ariaFlowtoCandidate: ( for (const child of children) { if (previousChild !== null) { // Set the `child` as the following sibling of `previousChild`. - memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child)); + memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child as ResolverEvent)); } // Set the child as the previous child. - previousChild = child; + previousChild = child as ResolverEvent; } if (previousChild) { @@ -553,7 +556,7 @@ export const nodesAndEdgelines: ( maxX, maxY, }); - const visibleProcessNodePositions = new Map( + const visibleProcessNodePositions = new Map( entities .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') .map((node) => [node.entity, node.position]) diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts index ad06ddf36161ab..8dd15b1a44d0c0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { layout } from './selectors'; import { ResolverState } from '../types'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; const animationDuration = 1000; @@ -20,7 +20,7 @@ export function animateProcessIntoView( process: ResolverEvent ): ResolverState { const { processNodePositions } = layout(state); - const position = processNodePositions.get(process); + const position = processNodePositions.get(process as SafeResolverEvent); if (position) { return { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index df365a078b27f7..dfbc6bd290686f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -13,6 +13,7 @@ import { mockTreeWith2AncestorsAndNoChildren, mockTreeWithNoAncestorsAnd2Children, } from './mocks/resolver_tree'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; describe('resolver selectors', () => { const actions: ResolverAction[] = []; @@ -114,7 +115,9 @@ describe('resolver selectors', () => { // find the position of the second child const secondChild = selectors.processEventForID(state())(secondChildID); - const positionOfSecondChild = layout.processNodePositions.get(secondChild!)!; + const positionOfSecondChild = layout.processNodePositions.get( + secondChild as SafeResolverEvent + )!; // the child is indexed by an AABB that extends -720/2 to the left const leftSideOfSecondChildAABB = positionOfSecondChild[0] - 720 / 2; @@ -130,19 +133,25 @@ describe('resolver selectors', () => { it('the origin should be in view', () => { const origin = selectors.processEventForID(state())(originID)!; expect( - selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(origin) + selectors + .visibleNodesAndEdgeLines(state())(0) + .processNodePositions.has(origin as SafeResolverEvent) ).toBe(true); }); it('the first child should be in view', () => { const firstChild = selectors.processEventForID(state())(firstChildID)!; expect( - selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(firstChild) + selectors + .visibleNodesAndEdgeLines(state())(0) + .processNodePositions.has(firstChild as SafeResolverEvent) ).toBe(true); }); it('the second child should not be in view', () => { const secondChild = selectors.processEventForID(state())(secondChildID)!; expect( - selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(secondChild) + selectors + .visibleNodesAndEdgeLines(state())(0) + .processNodePositions.has(secondChild as SafeResolverEvent) ).toBe(false); }); it('should return nothing as the flowto for the first child', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 87ef8d5d095ef0..70a461909a99bd 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -9,8 +9,8 @@ import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; import { ResolverState, IsometricTaxiLayout } from '../types'; -import { uniquePidForProcess } from '../models/process_event'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; +import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; /** * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. @@ -271,9 +271,14 @@ export const ariaFlowtoNodeID: ( const { processNodePositions } = visibleNodesAndEdgeLinesAtTime(time); // get a `Set` containing their node IDs - const nodesVisibleAtTime: Set = new Set( - [...processNodePositions.keys()].map(uniquePidForProcess) - ); + const nodesVisibleAtTime: Set = new Set(); + // NB: in practice, any event that has been graphed is guaranteed to have an entity_id + for (const visibleEvent of processNodePositions.keys()) { + const nodeID = entityIDSafeVersion(visibleEvent); + if (nodeID !== undefined) { + nodesVisibleAtTime.add(nodeID); + } + } // return the ID of `nodeID`'s following sibling, if it is visible return (nodeID: string): string | null => { diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index c2871fdceb20ad..30634e722050fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -11,10 +11,10 @@ import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; import { ResolverAction } from './store/actions'; import { - ResolverEvent, ResolverRelatedEvents, ResolverTree, ResolverEntityIndex, + SafeResolverEvent, } from '../../common/endpoint/types'; /** @@ -155,7 +155,7 @@ export interface IndexedEdgeLineSegment extends BBox { */ export interface IndexedProcessNode extends BBox { type: 'processNode'; - entity: ResolverEvent; + entity: SafeResolverEvent; position: Vector2; } @@ -280,21 +280,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's ordered children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of `ProcessEvents` (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; export type DurationTypes = | 'millisecond' @@ -346,11 +346,11 @@ export interface EdgeLineSegment { * Used to provide pre-calculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: ResolverEvent; + process: SafeResolverEvent; width: number; } & ( | { - parent: ResolverEvent; + parent: SafeResolverEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; @@ -433,7 +433,7 @@ export interface IsometricTaxiLayout { /** * A map of events to position. Each event represents its own node. */ - processNodePositions: Map; + processNodePositions: Map; /** * A map of edge-line segments, which graphically connect nodes. */ @@ -442,7 +442,7 @@ export interface IsometricTaxiLayout { /** * defines the aria levels for nodes. */ - ariaLevels: Map; + ariaLevels: Map; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index a965f06c049262..bbff2388af8b71 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -20,7 +20,7 @@ import { SymbolDefinitions, useResolverTheme } from './assets'; import { useStateSyncingActions } from './use_state_syncing_actions'; import { useResolverQueryParams } from './use_resolver_query_params'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; -import { entityId } from '../../../common/endpoint/models/event'; +import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; /** @@ -107,7 +107,7 @@ export const ResolverMap = React.memo(function ({ /> ))} {[...processNodePositions].map(([processEvent, position]) => { - const processEntityId = entityId(processEvent); + const processEntityId = entityIDSafeVersion(processEvent); return ( unknown; -}) { - interface ProcessTableView { - name: string; - timestamp?: Date; - event: ResolverEvent; - } - - const dispatch = useResolverDispatch(); - const { timestamp } = useContext(SideEffectContext); - const isProcessTerminated = useSelector(selectors.isProcessTerminated); - const handleBringIntoViewClick = useCallback( - (processTableViewItem) => { - dispatch({ - type: 'userBroughtProcessIntoView', - payload: { - time: timestamp(), - process: processTableViewItem.event, - }, - }); - pushToQueryParams({ crumbId: event.entityId(processTableViewItem.event), crumbEvent: '' }); - }, - [dispatch, timestamp, pushToQueryParams] - ); - - const columns = useMemo>>( - () => [ - { - field: 'name', - name: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle', - { - defaultMessage: 'Process Name', - } - ), - sortable: true, - truncateText: true, - render(name: string, item: ProcessTableView) { - const entityId = event.entityId(item.event); - const isTerminated = isProcessTerminated(entityId); - return name === '' ? ( - - {i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription', - { - defaultMessage: 'Value is missing', - } - )} - - ) : ( - { - handleBringIntoViewClick(item); - pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' }); - }} - > - - {name} - - ); - }, - }, - { - field: 'timestamp', - name: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle', - { - defaultMessage: 'Timestamp', - } - ), - dataType: 'date', - sortable: true, - render(eventDate?: Date) { - return eventDate ? ( - formatter.format(eventDate) - ) : ( - - {i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel', - { - defaultMessage: 'invalid', - } - )} - - ); - }, - }, - ], - [pushToQueryParams, handleBringIntoViewClick, isProcessTerminated] - ); - - const { processNodePositions } = useSelector(selectors.layout); - const processTableView: ProcessTableView[] = useMemo( - () => - [...processNodePositions.keys()].map((processEvent) => { - let dateTime; - const eventTime = event.eventTimestamp(processEvent); - const name = event.eventName(processEvent); - if (eventTime) { - const date = new Date(eventTime); - if (isFinite(date.getTime())) { - dateTime = date; - } - } - return { - name, - timestamp: dateTime, - event: processEvent, - }; - }), - [processNodePositions] - ); - const numberOfProcesses = processTableView.length; - - const crumbs = useMemo(() => { - return [ - { - text: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processListWithCounts.events', - { - defaultMessage: 'All Process Events', - } - ), - onClick: () => {}, - }, - ]; - }, []); - - const children = useSelector(selectors.hasMoreChildren); - const ancestors = useSelector(selectors.hasMoreAncestors); - const showWarning = children === true || ancestors === true; - return ( - <> - - {showWarning && } - - - data-test-subj="resolver:panel:process-list" - items={processTableView} - columns={columns} - sorting - /> - - ); -}); -ProcessListWithCounts.displayName = 'ProcessListWithCounts'; +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useContext, useCallback, useMemo } from 'react'; +import { + EuiBasicTableColumn, + EuiBadge, + EuiButtonEmpty, + EuiSpacer, + EuiInMemoryTable, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import * as event from '../../../../common/endpoint/models/event'; +import * as selectors from '../../store/selectors'; +import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities'; +import { useResolverDispatch } from '../use_resolver_dispatch'; +import { SideEffectContext } from '../side_effect_context'; +import { CubeForProcess } from './process_cube_icon'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { LimitWarning } from '../limit_warnings'; + +const StyledLimitWarning = styled(LimitWarning)` + flex-flow: row wrap; + display: block; + align-items: baseline; + margin-top: 1em; + + & .euiCallOutHeader { + display: inline; + margin-right: 0.25em; + } + + & .euiText { + display: inline; + } + + & .euiText p { + display: inline; + } +`; + +/** + * The "default" view for the panel: A list of all the processes currently in the graph. + * + * @param {function} pushToQueryparams A function to update the hash value in the URL to control panel state + */ +export const ProcessListWithCounts = memo(function ProcessListWithCounts({ + pushToQueryParams, +}: { + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; +}) { + interface ProcessTableView { + name?: string; + timestamp?: Date; + event: SafeResolverEvent; + } + + const dispatch = useResolverDispatch(); + const { timestamp } = useContext(SideEffectContext); + const isProcessTerminated = useSelector(selectors.isProcessTerminated); + const handleBringIntoViewClick = useCallback( + (processTableViewItem) => { + dispatch({ + type: 'userBroughtProcessIntoView', + payload: { + time: timestamp(), + process: processTableViewItem.event, + }, + }); + pushToQueryParams({ crumbId: event.entityId(processTableViewItem.event), crumbEvent: '' }); + }, + [dispatch, timestamp, pushToQueryParams] + ); + + const columns = useMemo>>( + () => [ + { + field: 'name', + name: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle', + { + defaultMessage: 'Process Name', + } + ), + sortable: true, + truncateText: true, + render(name: string, item: ProcessTableView) { + const entityID = event.entityIDSafeVersion(item.event); + const isTerminated = entityID === undefined ? false : isProcessTerminated(entityID); + return name === '' ? ( + + {i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription', + { + defaultMessage: 'Value is missing', + } + )} + + ) : ( + { + handleBringIntoViewClick(item); + pushToQueryParams({ + // Take the user back to the list of nodes if this node has no ID + crumbId: event.entityIDSafeVersion(item.event) ?? '', + crumbEvent: '', + }); + }} + > + + {name} + + ); + }, + }, + { + field: 'timestamp', + name: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle', + { + defaultMessage: 'Timestamp', + } + ), + dataType: 'date', + sortable: true, + render(eventDate?: Date) { + return eventDate ? ( + formatter.format(eventDate) + ) : ( + + {i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel', + { + defaultMessage: 'invalid', + } + )} + + ); + }, + }, + ], + [pushToQueryParams, handleBringIntoViewClick, isProcessTerminated] + ); + + const { processNodePositions } = useSelector(selectors.layout); + const processTableView: ProcessTableView[] = useMemo( + () => + [...processNodePositions.keys()].map((processEvent) => { + let dateTime; + const eventTime = event.timestampSafeVersion(processEvent); + const name = event.processNameSafeVersion(processEvent); + if (eventTime) { + const date = new Date(eventTime); + if (isFinite(date.getTime())) { + dateTime = date; + } + } + return { + name, + timestamp: dateTime, + event: processEvent, + }; + }), + [processNodePositions] + ); + const numberOfProcesses = processTableView.length; + + const crumbs = useMemo(() => { + return [ + { + text: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.processListWithCounts.events', + { + defaultMessage: 'All Process Events', + } + ), + onClick: () => {}, + }, + ]; + }, []); + + const children = useSelector(selectors.hasMoreChildren); + const ancestors = useSelector(selectors.hasMoreAncestors); + const showWarning = children === true || ancestors === true; + return ( + <> + + {showWarning && } + + + data-test-subj="resolver:panel:process-list" + items={processTableView} + columns={columns} + sorting + /> + + ); +}); +ProcessListWithCounts.displayName = 'ProcessListWithCounts'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 24de45ee894dcb..2a5d91028d9f55 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -14,10 +14,9 @@ import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3 } from '../types'; import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; -import * as processEventModel from '../models/process_event'; import * as selectors from '../store/selectors'; import { useResolverQueryParams } from './use_resolver_query_params'; @@ -85,7 +84,7 @@ const UnstyledProcessEventDot = React.memo( /** * An event which contains details about the process node. */ - event: ResolverEvent; + event: SafeResolverEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -114,7 +113,11 @@ const UnstyledProcessEventDot = React.memo( // Node (html id=) IDs const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); const selectedNode = useSelector(selectors.selectedNode); - const nodeID = processEventModel.uniquePidForProcess(event); + const nodeID: string | undefined = eventModel.entityIDSafeVersion(event); + if (nodeID === undefined) { + // NB: this component should be taking nodeID as a `string` instead of handling this logic here + throw new Error('Tried to render a node with no ID'); + } const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. @@ -287,7 +290,9 @@ const UnstyledProcessEventDot = React.memo( ? subMenuAssets.initialMenuStatus : relatedEventOptions; - const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)(event); + const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)( + event as ResolverEvent + ); /* eslint-disable jsx-a11y/click-events-have-key-events */ /** @@ -398,11 +403,11 @@ const UnstyledProcessEventDot = React.memo( maxWidth: `${isShowingEventActions ? 400 : 210 * xScale}px`, }} tabIndex={-1} - title={eventModel.eventName(event)} + title={eventModel.processNameSafeVersion(event)} > - {eventModel.eventName(event)} + {eventModel.processNameSafeVersion(event)} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index e74502243ffc8c..5f1e5f18e575dd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -20,7 +20,7 @@ import { SymbolDefinitions, useResolverTheme } from './assets'; import { useStateSyncingActions } from './use_state_syncing_actions'; import { useResolverQueryParams } from './use_resolver_query_params'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; -import { entityId } from '../../../common/endpoint/models/event'; +import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; import { ResolverProps } from '../types'; @@ -114,7 +114,7 @@ export const ResolverWithoutProviders = React.memo( ) )} {[...processNodePositions].map(([processEvent, position]) => { - const processEntityId = entityId(processEvent); + const processEntityId = entityIDSafeVersion(processEvent); return ( { } const processes: ResolverEvent[] = [ ...selectors.layout(store.getState()).processNodePositions.keys(), - ]; + ] as ResolverEvent[]; process = processes[processes.length - 1]; if (!process) { throw new Error('missing the process to bring into view'); From 3064c6eceb262d80174b6a13d356325b44182500 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Aug 2020 15:34:32 +0200 Subject: [PATCH 06/31] Improve state sync error handling (#74264) Fixes #71461 regression since 7.7 New state syncing utils didn't properly handle errors, Errors happening during URL parsing or writing wasn't handled, so state syncing could stop or in worth case blow out. (see #71461) There are not much scenarios where missing proper error handling could really impact users, except the one described in #71461: Kibana users state:storeInSessionStorage Users often intuitively share hashed dashboard urls directly When someone opens those urls - there is a blank screen with warning In 7.6 - dashboard would still load with default state. Since 7.7 these still could be achieved by removing query params for URL, but it is not obvious for regular users. This PR makes sure that behaviour is similar to one we had before 7.7. Co-authored-by: Elastic Machine --- ...lic-state_sync.createkbnurlstatestorage.md | 4 +- .../public/application/legacy_app.js | 2 + .../public/application/angular/context.js | 1 + .../application/angular/context_state.ts | 11 +++ .../public/application/angular/discover.js | 1 + .../application/angular/discover_state.ts | 11 +++ .../kibana_utils/docs/state_sync/README.md | 1 + .../docs/state_sync/error_handling.md | 6 ++ .../state_sync/storages/kbn_url_storage.md | 57 ++++++++++++++- src/plugins/kibana_utils/public/index.ts | 1 + .../public/state_management/url/errors.ts | 62 ++++++++++++++++ .../public/state_management/url/index.ts | 1 + .../state_management/url/kbn_url_storage.ts | 10 +-- .../public/state_sync/public.api.md | 4 +- .../create_kbn_url_state_storage.test.ts | 73 ++++++++++++++++++- .../create_kbn_url_state_storage.ts | 40 ++++++++-- src/plugins/timelion/public/app.js | 3 +- src/plugins/visualize/public/plugin.ts | 8 +- .../functional/apps/discover/_shared_links.js | 62 ++++++++++------ test/functional/services/toasts.ts | 10 +++ x-pack/plugins/lens/public/app_plugin/app.tsx | 3 + .../maps/public/routing/maps_router.js | 13 +++- .../monitoring/public/angular/app_modules.ts | 11 ++- x-pack/plugins/monitoring/public/url_state.ts | 8 +- 24 files changed, 355 insertions(+), 48 deletions(-) create mode 100644 src/plugins/kibana_utils/docs/state_sync/error_handling.md create mode 100644 src/plugins/kibana_utils/public/state_management/url/errors.ts diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md index 22f70ce22b5745..478ba2d409acd2 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md @@ -9,8 +9,10 @@ Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_ Signature: ```typescript -createKbnUrlStateStorage: ({ useHash, history }?: { +createKbnUrlStateStorage: ({ useHash, history, onGetError, onSetError, }?: { useHash: boolean; history?: History | undefined; + onGetError?: ((error: Error) => void) | undefined; + onSetError?: ((error: Error) => void) | undefined; }) => IKbnUrlStateStorage ``` diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 8b8fdcb7a76acc..abe04fb8bd7e31 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -30,6 +30,7 @@ import { createKbnUrlStateStorage, redirectWhenMissing, SavedObjectNotFound, + withNotifyOnErrors, } from '../../../kibana_utils/public'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; @@ -65,6 +66,7 @@ export function initDashboardApp(app, deps) { createKbnUrlStateStorage({ history, useHash: deps.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(deps.core.notifications.toasts), }) ); diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index a6f591eebb52d0..6223090aa9f971 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -83,6 +83,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { timeFieldName: indexPattern.timeFieldName, storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), history: getServices().history(), + toasts: getServices().core.notifications.toasts, }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 7a92a6ace125b3..5b05d8729c41dd 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -18,11 +18,13 @@ */ import _ from 'lodash'; import { History } from 'history'; +import { NotificationsStart } from 'kibana/public'; import { createStateContainer, createKbnUrlStateStorage, syncStates, BaseStateContainer, + withNotifyOnErrors, } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; @@ -74,6 +76,13 @@ interface GetStateParams { * History instance to use */ history: History; + + /** + * Core's notifications.toasts service + * In case it is passed in, + * kbnUrlStateStorage will use it notifying about inner errors + */ + toasts?: NotificationsStart['toasts']; } interface GetStateReturn { @@ -123,10 +132,12 @@ export function getState({ timeFieldName, storeInSessionStorage = false, history, + toasts, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, history, + ...(toasts && withNotifyOnErrors(toasts)), }); const globalStateInitial = stateStorage.get(GLOBAL_STATE_URL_KEY) as GlobalState; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 4a27f261a62206..22da3e877054ae 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -220,6 +220,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise defaultAppState: getStateDefaults(), storeInSessionStorage: config.get('state:storeInSessionStorage'), history, + toasts: core.notifications.toasts, }); if (appStateContainer.getState().index !== $scope.indexPattern.id) { //used index pattern is different than the given by url/state which is invalid diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 46500d9fdf85e2..ff8fb9f80a7237 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -18,12 +18,14 @@ */ import { isEqual } from 'lodash'; import { History } from 'history'; +import { NotificationsStart } from 'kibana/public'; import { createStateContainer, createKbnUrlStateStorage, syncState, ReduxLikeStateContainer, IKbnUrlStateStorage, + withNotifyOnErrors, } from '../../../../kibana_utils/public'; import { esFilters, Filter, Query } from '../../../../data/public'; import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; @@ -68,6 +70,13 @@ interface GetStateParams { * Browser history */ history: History; + + /** + * Core's notifications.toasts service + * In case it is passed in, + * kbnUrlStateStorage will use it notifying about inner errors + */ + toasts?: NotificationsStart['toasts']; } export interface GetStateReturn { @@ -122,10 +131,12 @@ export function getState({ defaultAppState = {}, storeInSessionStorage = false, history, + toasts, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, history, + ...(toasts && withNotifyOnErrors(toasts)), }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index acfe6dcf76fe97..c84bf7f2363304 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -58,3 +58,4 @@ To run them, start kibana with `--run-examples` flag. - [On-the-fly state migrations](./on_fly_state_migrations.md). - [syncStates helper](./sync_states.md). - [Helpers for Data plugin (syncing TimeRange, RefreshInterval and Filters)](./data_plugin_helpers.md). +- [Error handling](./error_handling.md) diff --git a/src/plugins/kibana_utils/docs/state_sync/error_handling.md b/src/plugins/kibana_utils/docs/state_sync/error_handling.md new file mode 100644 index 00000000000000..b12e1040af260b --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_sync/error_handling.md @@ -0,0 +1,6 @@ +# Error handling + +State syncing util doesn't have specific api for handling errors. +It expects that errors are handled on storage level. + +see [KbnUrlStateStorage](./storages/kbn_url_storage.md#) error handling section for details. diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md index 3a31f5a326edb8..ec27895eed6665 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md @@ -65,7 +65,7 @@ To prevent bugs caused by missing history updates, make sure your app uses one i For example, if you use `react-router`: ```tsx -const App = props => { +const App = (props) => { useEffect(() => { const stateStorage = createKbnUrlStateStorage({ useHash: props.uiSettings.get('state:storeInSessionStorage'), @@ -160,3 +160,58 @@ const { start, stop } = syncStates([ ; ``` + +### Error handling + +Errors could occur both during `kbnUrlStateStorage.get()` and `kbnUrlStateStorage.set()` + +#### Handling kbnUrlStateStorage.get() errors + +Possible error scenarios during `kbnUrlStateStorage.get()`: + +1. Rison in URL is malformed. Parsing exception. +2. useHash is enabled and current hash is missing in `sessionStorage` + +In all the cases error is handled internally and `kbnUrlStateStorage.get()` returns `null`, just like if there is no state in the URL anymore + +You can pass callback to get notified about errors. Use it, for example, for notifying users + +```ts +const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + onGetError: (error) => { + alert(error.message); + }, +}); +``` + +#### Handling kbnUrlStateStorage.set() errors + +Possible errors during `kbnUrlStateStorage.set()`: + +1. `useHash` is enabled and can't store state in `sessionStorage` (overflow or no access) + +In all the cases error is handled internally and URL update is skipped + +You can pass callback to get notified about errors. Use it, for example, for notifying users: + +```ts +const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + onSetError: (error) => { + alert(error.message); + }, +}); +``` + +#### Helper to integrate with core.notifications.toasts + +The most common scenario is to notify users about issues with state syncing using toast service from core +There is a convenient helper for this: + +```ts +const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + ...withNotifyOnErrors(core.notifications.toasts), +}); +``` diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index e2d6ae647abb11..d1c9eec0e9906e 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -57,6 +57,7 @@ export { getStateFromKbnUrl, getStatesFromKbnUrl, setStateToKbnUrl, + withNotifyOnErrors, } from './state_management/url'; export { syncState, diff --git a/src/plugins/kibana_utils/public/state_management/url/errors.ts b/src/plugins/kibana_utils/public/state_management/url/errors.ts new file mode 100644 index 00000000000000..b8b6523e8070c5 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/errors.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; + +export const restoreUrlErrorTitle = i18n.translate( + 'kibana_utils.stateManagement.url.restoreUrlErrorTitle', + { + defaultMessage: `Error restoring state from URL`, + } +); + +export const saveStateInUrlErrorTitle = i18n.translate( + 'kibana_utils.stateManagement.url.saveStateInUrlErrorTitle', + { + defaultMessage: `Error saving state in URL`, + } +); + +/** + * Helper for configuring {@link IKbnUrlStateStorage} to notify about inner errors + * + * @example + * ```ts + * const kbnUrlStateStorage = createKbnUrlStateStorage({ + * history, + * ...withNotifyOnErrors(core.notifications.toast)) + * } + * ``` + * @param toast - toastApi from core.notifications.toasts + */ +export const withNotifyOnErrors = (toasts: NotificationsStart['toasts']) => { + return { + onGetError: (error: Error) => { + toasts.addError(error, { + title: restoreUrlErrorTitle, + }); + }, + onSetError: (error: Error) => { + toasts.addError(error, { + title: saveStateInUrlErrorTitle, + }); + }, + }; +}; diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index e28d183c6560a2..66fecd723e3bab 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -27,3 +27,4 @@ export { } from './kbn_url_storage'; export { createKbnUrlTracker } from './kbn_url_tracker'; export { createUrlTracker } from './url_tracker'; +export { withNotifyOnErrors, saveStateInUrlErrorTitle, restoreUrlErrorTitle } from './errors'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index d9149095a2fa23..fefd5f668c6b3a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -103,7 +103,7 @@ export function setStateToKbnUrl( export interface IKbnUrlControls { /** * Listen for url changes - * @param cb - get's called when url has been changed + * @param cb - called when url has been changed */ listen: (cb: () => void) => () => void; @@ -142,12 +142,12 @@ export interface IKbnUrlControls { */ cancel: () => void; } -export type UrlUpdaterFnType = (currentUrl: string) => string; +export type UrlUpdaterFnType = (currentUrl: string) => string | undefined; export const createKbnUrlControls = ( history: History = createBrowserHistory() ): IKbnUrlControls => { - const updateQueue: Array<(currentUrl: string) => string> = []; + const updateQueue: UrlUpdaterFnType[] = []; // if we should replace or push with next async update, // if any call in a queue asked to push, then we should push @@ -188,7 +188,7 @@ export const createKbnUrlControls = ( function getPendingUrl() { if (updateQueue.length === 0) return undefined; const resultUrl = updateQueue.reduce( - (url, nextUpdate) => nextUpdate(url), + (url, nextUpdate) => nextUpdate(url) ?? url, getCurrentUrl(history) ); @@ -201,7 +201,7 @@ export const createKbnUrlControls = ( cb(); }), update: (newUrl: string, replace = false) => updateUrl(newUrl, replace), - updateAsync: (updater: (currentUrl: string) => string, replace = false) => { + updateAsync: (updater: UrlUpdaterFnType, replace = false) => { updateQueue.push(updater); if (shouldReplace) { shouldReplace = replace; diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md index ae8c0e8e401b8e..a4dfea82cdb59c 100644 --- a/src/plugins/kibana_utils/public/state_sync/public.api.md +++ b/src/plugins/kibana_utils/public/state_sync/public.api.md @@ -8,9 +8,11 @@ import { History } from 'history'; import { Observable } from 'rxjs'; // @public -export const createKbnUrlStateStorage: ({ useHash, history }?: { +export const createKbnUrlStateStorage: ({ useHash, history, onGetError, onSetError, }?: { useHash: boolean; history?: History | undefined; + onGetError?: ((error: Error) => void) | undefined; + onSetError?: ((error: Error) => void) | undefined; }) => IKbnUrlStateStorage; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index cc708d14ea8b5f..e222af91d77297 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import '../../storage/hashed_item_store/mock'; +import { mockStorage } from '../../storage/hashed_item_store/mock'; import { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_state_storage'; import { History, createBrowserHistory } from 'history'; import { takeUntil, toArray } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { ScopedHistory } from '../../../../../core/public'; +import { withNotifyOnErrors } from '../../state_management/url'; +import { coreMock } from '../../../../../core/public/mocks'; describe('KbnUrlStateStorage', () => { describe('useHash: false', () => { @@ -93,6 +95,37 @@ describe('KbnUrlStateStorage', () => { expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); }); + + it("shouldn't throw in case of parsing error", async () => { + const key = '_s'; + history.replace(`/#?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(urlStateStorage.get(key)).toBeNull(); + }); + + it('should notify about errors', () => { + const cb = jest.fn(); + urlStateStorage = createKbnUrlStateStorage({ useHash: false, history, onGetError: cb }); + const key = '_s'; + history.replace(`/#?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(cb).toBeCalledWith(expect.any(Error)); + }); + + describe('withNotifyOnErrors integration', () => { + test('toast is shown', () => { + const toasts = coreMock.createStart().notifications.toasts; + urlStateStorage = createKbnUrlStateStorage({ + useHash: true, + history, + ...withNotifyOnErrors(toasts), + }); + const key = '_s'; + history.replace(`/#?${key}=(ok:2,test:`); // malformed rison + expect(() => urlStateStorage.get(key)).not.toThrow(); + expect(toasts.addError).toBeCalled(); + }); + }); }); describe('useHash: true', () => { @@ -128,6 +161,44 @@ describe('KbnUrlStateStorage', () => { expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); }); + + describe('hashStorage overflow exception', () => { + let oldLimit: number; + beforeAll(() => { + oldLimit = mockStorage.getStubbedSizeLimit(); + mockStorage.clear(); + mockStorage.setStubbedSizeLimit(0); + }); + afterAll(() => { + mockStorage.setStubbedSizeLimit(oldLimit); + }); + + it("shouldn't throw in case of error", async () => { + expect(() => urlStateStorage.set('_s', { test: 'test' })).not.toThrow(); + await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects + expect(getCurrentUrl()).toBe('/'); // url wasn't updated with hash + }); + + it('should notify about errors', async () => { + const cb = jest.fn(); + urlStateStorage = createKbnUrlStateStorage({ useHash: true, history, onSetError: cb }); + await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects + expect(cb).toBeCalledWith(expect.any(Error)); + }); + + describe('withNotifyOnErrors integration', () => { + test('toast is shown', async () => { + const toasts = coreMock.createStart().notifications.toasts; + urlStateStorage = createKbnUrlStateStorage({ + useHash: true, + history, + ...withNotifyOnErrors(toasts), + }); + await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects + expect(toasts.addError).toBeCalled(); + }); + }); + }); }); describe('ScopedHistory integration', () => { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 0c74e1eb9f4211..460720b98e30f1 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { map, share } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { catchError, map, share } from 'rxjs/operators'; import { History } from 'history'; import { IStateStorage } from './types'; import { @@ -68,7 +68,19 @@ export interface IKbnUrlStateStorage extends IStateStorage { * @public */ export const createKbnUrlStateStorage = ( - { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false } + { + useHash = false, + history, + onGetError, + onSetError, + }: { + useHash: boolean; + history?: History; + onGetError?: (error: Error) => void; + onSetError?: (error: Error) => void; + } = { + useHash: false, + } ): IKbnUrlStateStorage => { const url = createKbnUrlControls(history); return { @@ -78,15 +90,23 @@ export const createKbnUrlStateStorage = ( { replace = false }: { replace: boolean } = { replace: false } ) => { // syncState() utils doesn't wait for this promise - return url.updateAsync( - (currentUrl) => setStateToKbnUrl(key, state, { useHash }, currentUrl), - replace - ); + return url.updateAsync((currentUrl) => { + try { + return setStateToKbnUrl(key, state, { useHash }, currentUrl); + } catch (error) { + if (onSetError) onSetError(error); + } + }, replace); }, get: (key) => { // if there is a pending url update, then state will be extracted from that pending url, // otherwise current url will be used to retrieve state from - return getStateFromKbnUrl(key, url.getPendingUrl()); + try { + return getStateFromKbnUrl(key, url.getPendingUrl()); + } catch (e) { + if (onGetError) onGetError(e); + return null; + } }, change$: (key: string) => new Observable((observer) => { @@ -99,6 +119,10 @@ export const createKbnUrlStateStorage = ( }; }).pipe( map(() => getStateFromKbnUrl(key)), + catchError((error) => { + if (onGetError) onGetError(error); + return of(null); + }), share() ), flush: ({ replace = false }: { replace?: boolean } = {}) => { diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 0294e71084f988..614a7539de44c7 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../kibana_utils/public'; +import { createKbnUrlStateStorage, withNotifyOnErrors } from '../../kibana_utils/public'; import { syncQueryStateWithUrl } from '../../data/public'; import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; @@ -63,6 +63,7 @@ export function initTimelionApp(app, deps) { createKbnUrlStateStorage({ history, useHash: deps.core.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(deps.core.notifications.toasts), }) ); app.config(watchMultiDecorator); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index fd9a67599414f1..3299319e613a01 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -31,7 +31,12 @@ import { ScopedHistory, } from 'kibana/public'; -import { Storage, createKbnUrlTracker, createKbnUrlStateStorage } from '../../kibana_utils/public'; +import { + Storage, + createKbnUrlTracker, + createKbnUrlStateStorage, + withNotifyOnErrors, +} from '../../kibana_utils/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { SharePluginStart } from '../../share/public'; @@ -150,6 +155,7 @@ export class VisualizePlugin kbnUrlStateStorage: createKbnUrlStateStorage({ history, useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), }), kibanaLegacy: pluginsStart.kibanaLegacy, pluginInitializerContext: this.initializerContext, diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 5c6a70450a0aa1..94409a94e9257f 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'share', 'timePicker']); const browser = getService('browser'); + const toasts = getService('toasts'); describe('shared links', function describeIndexTests() { let baseUrl; @@ -132,28 +133,47 @@ export default function ({ getService, getPageObjects }) { await teardown(); }); - describe('permalink', function () { - it('should allow for copying the snapshot URL as a short URL and should open it', async function () { - const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); - await PageObjects.share.checkShortenUrl(); - let actualUrl; - await retry.try(async () => { - actualUrl = await PageObjects.share.getSharedUrl(); - expect(actualUrl).to.match(re); - }); + it('should allow for copying the snapshot URL as a short URL and should open it', async function () { + const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); + await PageObjects.share.checkShortenUrl(); + let actualUrl; + await retry.try(async () => { + actualUrl = await PageObjects.share.getSharedUrl(); + expect(actualUrl).to.match(re); + }); - const actualTime = await PageObjects.timePicker.getTimeConfig(); - - await browser.clearSessionStorage(); - await browser.get(actualUrl, false); - await retry.waitFor('shortUrl resolves and opens', async () => { - const resolvedUrl = await browser.getCurrentUrl(); - expect(resolvedUrl).to.match(/discover/); - const resolvedTime = await PageObjects.timePicker.getTimeConfig(); - expect(resolvedTime.start).to.equal(actualTime.start); - expect(resolvedTime.end).to.equal(actualTime.end); - return true; - }); + const actualTime = await PageObjects.timePicker.getTimeConfig(); + + await browser.clearSessionStorage(); + await browser.get(actualUrl, false); + await retry.waitFor('shortUrl resolves and opens', async () => { + const resolvedUrl = await browser.getCurrentUrl(); + expect(resolvedUrl).to.match(/discover/); + const resolvedTime = await PageObjects.timePicker.getTimeConfig(); + expect(resolvedTime.start).to.equal(actualTime.start); + expect(resolvedTime.end).to.equal(actualTime.end); + return true; + }); + }); + + it("sharing hashed url shouldn't crash the app", async () => { + const currentUrl = await browser.getCurrentUrl(); + const timeBeforeReload = await PageObjects.timePicker.getTimeConfig(); + await browser.clearSessionStorage(); + await browser.get(currentUrl, false); + await retry.waitFor('discover to open', async () => { + const resolvedUrl = await browser.getCurrentUrl(); + expect(resolvedUrl).to.match(/discover/); + const { message } = await toasts.getErrorToast(); + expect(message).to.contain( + 'Unable to completely restore the URL, be sure to use the share functionality.' + ); + await toasts.dismissAllToasts(); + const timeAfterReload = await PageObjects.timePicker.getTimeConfig(); + expect(timeBeforeReload.start).not.to.be(timeAfterReload.start); + expect(timeBeforeReload.end).not.to.be(timeAfterReload.end); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + return true; }); }); }); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index 92f1f726fa039a..a70e4ba464ae85 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -53,6 +53,16 @@ export function ToastsProvider({ getService }: FtrProviderContext) { await dismissButton.click(); } + public async dismissAllToasts() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); + for (const toast of toasts) { + await toast.moveMouseTo(); + const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } + } + private async getToastElement(index: number) { const list = await this.getGlobalToastList(); return await list.findByCssSelector(`.euiToast:nth-child(${index})`); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4a8694862642b5..4a6dbd4a91fbfe 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -19,6 +19,7 @@ import { import { createKbnUrlStateStorage, IStorageWrapper, + withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { @@ -152,6 +153,7 @@ export function App({ const kbnUrlStateStorage = createKbnUrlStateStorage({ history, useHash: core.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(core.notifications.toasts), }); const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( data.query, @@ -166,6 +168,7 @@ export function App({ }, [ data.query.filterManager, data.query.timefilter.timefilter, + core.notifications.toasts, core.uiSettings, data.query, history, diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js index 30b2137399c1ea..9992bd7a92ab18 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.js +++ b/x-pack/plugins/maps/public/routing/maps_router.js @@ -7,8 +7,11 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Switch, Route, Redirect } from 'react-router-dom'; -import { getCoreI18n } from '../kibana_services'; -import { createKbnUrlStateStorage } from '../../../../../src/plugins/kibana_utils/public'; +import { getCoreI18n, getToasts } from '../kibana_services'; +import { + createKbnUrlStateStorage, + withNotifyOnErrors, +} from '../../../../../src/plugins/kibana_utils/public'; import { getStore } from './store_operations'; import { Provider } from 'react-redux'; import { LoadListAndRender } from './routes/list/load_list_and_render'; @@ -19,7 +22,11 @@ export let kbnUrlStateStorage; export async function renderApp(context, { appBasePath, element, history, onAppLeave }) { goToSpecifiedPath = (path) => history.push(path); - kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash: false, + history, + ...withNotifyOnErrors(getToasts()), + }); render(, element); diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index f3d77b196b26ec..499610045d7717 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -23,7 +23,7 @@ import { GlobalState } from '../url_state'; import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; // @ts-ignore -import { formatNumber, formatMetric } from '../lib/format_number'; +import { formatMetric, formatNumber } from '../lib/format_number'; // @ts-ignore import { extractIp } from '../lib/extract_ip'; // @ts-ignore @@ -65,7 +65,7 @@ export const localAppModule = ({ createLocalPrivateModule(); createLocalStorage(); createLocalConfigModule(core); - createLocalStateModule(query); + createLocalStateModule(query, core.notifications.toasts); createLocalTopNavModule(navigation); createHrefModule(core); createMonitoringAppServices(); @@ -97,7 +97,10 @@ function createMonitoringAppConfigConstants( keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); } -function createLocalStateModule(query: any) { +function createLocalStateModule( + query: MonitoringStartPluginDependencies['data']['query'], + toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'] +) { angular .module('monitoring/State', ['monitoring/Private']) .service('globalState', function ( @@ -106,7 +109,7 @@ function createLocalStateModule(query: any) { $location: ng.ILocationService ) { function GlobalStateProvider(this: any) { - const state = new GlobalState(query, $rootScope, $location, this); + const state = new GlobalState(query, toasts, $rootScope, $location, this); const initialState: any = state.getState(); for (const key in initialState) { if (!initialState.hasOwnProperty(key)) { diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index e53497d751f9bc..65e48223d7a647 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -23,6 +23,7 @@ import { IKbnUrlStateStorage, ISyncStateRef, syncState, + withNotifyOnErrors, } from '../../../../src/plugins/kibana_utils/public'; interface Route { @@ -71,6 +72,7 @@ export class GlobalState { constructor( queryService: MonitoringStartPluginDependencies['data']['query'], + toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService, externalState: RawObject @@ -78,7 +80,11 @@ export class GlobalState { this.timefilterRef = queryService.timefilter.timefilter; const history: History = createHashHistory(); - this.stateStorage = createKbnUrlStateStorage({ useHash: false, history }); + this.stateStorage = createKbnUrlStateStorage({ + useHash: false, + history, + ...withNotifyOnErrors(toasts), + }); const initialStateFromUrl = this.stateStorage.get(GLOBAL_STATE_KEY) as MonitoringAppState; From 11c74bc03f73c1bcec02d6ba7914bcfd442127ee Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 6 Aug 2020 15:35:53 +0200 Subject: [PATCH 07/31] [ML] Fix analytics list on management page. (#74254) The analytics job page in the Kibana management section didn't have the context provided by React Router and Kibana's own history object so the page crashed on load. The context is necessary to construct the correct URLs to navigate to the ML plugin itself. This PR fixes it by wrapping the management page in . Also adds functional tests to cover navigating to the jobs list pages in stack management. --- .../jobs_list_page/jobs_list_page.tsx | 96 +++++++++++-------- .../application/management/jobs_list/index.ts | 10 +- x-pack/test/functional/apps/ml/pages.ts | 12 +++ .../test/functional/services/ml/navigation.ts | 27 ++++++ 4 files changed, 101 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 33bb78c51e013e..0af6030df28b19 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useState, Fragment, FC } from 'react'; +import { Router } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -20,6 +21,8 @@ import { EuiTitle, } from '@elastic/eui'; +import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; + import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; @@ -30,6 +33,7 @@ import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/a import { AccessDeniedPage } from '../access_denied_page'; interface Tab { + 'data-test-subj': string; id: string; name: string; content: any; @@ -38,6 +42,7 @@ interface Tab { function getTabs(isMlEnabledInSpace: boolean): Tab[] { return [ { + 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', id: 'anomaly_detection_jobs', name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { defaultMessage: 'Anomaly detection', @@ -50,6 +55,7 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { ), }, { + 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', id: 'analytics_jobs', name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { defaultMessage: 'Analytics', @@ -67,7 +73,10 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { ]; } -export const JobsListPage: FC<{ coreStart: CoreStart }> = ({ coreStart }) => { +export const JobsListPage: FC<{ + coreStart: CoreStart; + history: ManagementAppMountParams['history']; +}> = ({ coreStart, history }) => { const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); @@ -128,46 +137,51 @@ export const JobsListPage: FC<{ coreStart: CoreStart }> = ({ coreStart }) => { return ( - - - - -

- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', - })} -

- - - - {currentTabId === 'anomaly_detection_jobs' - ? anomalyDetectionDocsLabel - : analyticsDocsLabel} - - - - - - - - {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { - defaultMessage: 'View machine learning analytics and anomaly detection jobs.', - })} - - - - {renderTabs()} - + + + + + +

+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { + defaultMessage: 'Machine Learning Jobs', + })} +

+
+ + + {currentTabId === 'anomaly_detection_jobs' + ? anomalyDetectionDocsLabel + : analyticsDocsLabel} + + +
+
+ + + + {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { + defaultMessage: 'View machine learning analytics and anomaly detection jobs.', + })} + + + + {renderTabs()} +
+
); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index 81190a412abc03..afea5a573b8b5f 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -14,8 +14,12 @@ import { getJobsListBreadcrumbs } from '../breadcrumbs'; import { setDependencyCache, clearCache } from '../../util/dependency_cache'; import './_index.scss'; -const renderApp = (element: HTMLElement, coreStart: CoreStart) => { - ReactDOM.render(React.createElement(JobsListPage, { coreStart }), element); +const renderApp = ( + element: HTMLElement, + history: ManagementAppMountParams['history'], + coreStart: CoreStart +) => { + ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element); return () => { unmountComponentAtNode(element); clearCache(); @@ -37,5 +41,5 @@ export async function mountApp( params.setBreadcrumbs(getJobsListBreadcrumbs()); - return renderApp(params.element, coreStart); + return renderApp(params.element, params.history, coreStart); } diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/pages.ts index e2c80c8dab5588..3691e6b1afcdc3 100644 --- a/x-pack/test/functional/apps/ml/pages.ts +++ b/x-pack/test/functional/apps/ml/pages.ts @@ -53,5 +53,17 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataVisualizer.assertDataVisualizerImportDataCardExists(); await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists(); }); + + it('should load the stack management with the ML menu item being present', async () => { + await ml.navigation.navigateToStackManagement(); + }); + + it('should load the jobs list page in stack management', async () => { + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + + it('should load the analytics jobs list page in stack management', async () => { + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + }); }); } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 9b67a369f055fb..f52197d4b2256d 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -23,6 +23,13 @@ export function MachineLearningNavigationProvider({ }); }, + async navigateToStackManagement() { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('management'); + await testSubjects.existOrFail('jobsListLink', { timeout: 2000 }); + }); + }, + async assertTabsExist(tabTypeSubject: string, areaSubjects: string[]) { await retry.tryForTime(10000, async () => { const allTabs = await testSubjects.findAll(`~${tabTypeSubject}`, 3); @@ -76,5 +83,25 @@ export function MachineLearningNavigationProvider({ async navigateToSettings() { await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings'); }, + + async navigateToStackManagementJobsListPage() { + // clicks the jobsListLink and loads the jobs list page + await testSubjects.click('jobsListLink'); + await retry.tryForTime(60 * 1000, async () => { + // verify that the overall page is present + await testSubjects.existOrFail('mlPageStackManagementJobsList'); + // verify that the default tab with the anomaly detection jobs list got loaded + await testSubjects.existOrFail('ml-jobs-list'); + }); + }, + + async navigateToStackManagementJobsListPageAnalyticsTab() { + // clicks the `Analytics` tab and loads the analytics list page + await testSubjects.click('mlStackManagementJobsListAnalyticsTab'); + await retry.tryForTime(60 * 1000, async () => { + // verify that the empty prompt for analytics jobs list got loaded + await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + }); + }, }; } From b3202a0e4cba43a54183f732d3092368f9e63fd1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 6 Aug 2020 17:02:27 +0200 Subject: [PATCH 08/31] fix tsvb validation (#74344) --- src/plugins/vis_type_timeseries/common/vis_schema.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index a462e488c67327..c1730e6a154358 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -119,6 +119,10 @@ export const metricsItems = schema.object({ type: stringRequired, value: stringOptionalNullable, values: schema.maybe(schema.nullable(schema.arrayOf(schema.nullable(schema.string())))), + size: stringOptionalNullable, + agg_with: stringOptionalNullable, + order: stringOptionalNullable, + order_by: stringOptionalNullable, }); const splitFiltersItems = schema.object({ From d00035422a554524b544fb5bd30cc6c78b346bbb Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Aug 2020 11:44:14 -0400 Subject: [PATCH 09/31] [SECURITY] Fix imports (#74528) * simple solution to avoid duplicate request * fix import of deepEqual --- .../public/common/containers/matrix_histogram/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 2122eab23957a2..c4702e915c0761 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import deepEqual from 'fast-deep-equal'; import { isEmpty } from 'lodash/fp'; import { useEffect, useMemo, useState, useRef } from 'react'; -import { deepEqual } from 'hoek'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { errorToToaster, useStateToaster } from '../../components/toasters'; From 0600f000be1986bc00f09aef73b738e27cf57437 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 6 Aug 2020 08:58:06 -0700 Subject: [PATCH 10/31] [DOCS] Add Kibana privileges to glossary (#74410) --- docs/glossary.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index 07c0bfcf35cb70..51470513198b9a 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -214,6 +214,13 @@ syslog, Apache, and other webserver logs. See [[k_glos]] == K +[[glossary-kibana-privileges]] {kib} privileges :: +// tag::kibana-privileges-def[] +Enable administrators to grant users read-only, read-write, or no access to +individual features within <> in {kib}. See +{kibana-ref}/kibana-privileges.html[{kib} privileges]. +// end::kibana-privileges-def[] + [[glossary-kql]] {kib} Query Language (KQL) :: // tag::kql-def[] The default language for querying in {kib}. KQL provides From d7644991d9cfde0ee1629b5cb72ffa9678d93c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 6 Aug 2020 18:06:13 +0200 Subject: [PATCH 11/31] [ILM] Convert node allocation component to TS and use hooks (#72888) * [ILM] Convert node allocation component to TS and use hooks * [ILM] Fix jest tests * [ILM] Fix i18n check * [ILM] Implement code review suggestions * [ILM] Fix type check, docs link and button maxWidth in NodeAllocation component * Fix internaliation error * [ILM] Change error message when unable to load node attributes * [ILM] Delete a period in error callout Co-authored-by: Elastic Machine --- .../__jest__/components/edit_policy.test.js | 147 ++++++++------- .../components/helpers/http_requests.ts | 48 +++++ .../sections/components/learn_more_link.js | 33 ---- .../sections/components/learn_more_link.tsx | 29 +++ .../node_allocation/{index.js => index.ts} | 2 +- .../node_allocation.container.js | 20 -- .../node_allocation/node_allocation.js | 120 ------------ .../node_allocation/node_allocation.tsx | 177 ++++++++++++++++++ .../{form_errors.js => form_errors.tsx} | 20 +- .../public/application/services/api.ts | 10 +- .../public/application/services/http.ts | 7 +- .../public/application/store/actions/nodes.js | 20 +- .../application/store/reducers/nodes.js | 8 - .../application/store/selectors/nodes.js | 20 -- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 16 files changed, 365 insertions(+), 298 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/{index.js => index.ts} (79%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{form_errors.js => form_errors.tsx} (57%) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 943f663a025d84..c6da347ed8cfef 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -5,16 +5,23 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import moment from 'moment-timezone'; import { Provider } from 'react-redux'; // axios has a $http like interface so using it to simulate $http import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import sinon from 'sinon'; import { findTestSubject } from '@elastic/eui/lib/test'; +import { init as initHttpRequests } from './helpers/http_requests'; +import { + notificationServiceMock, + fatalErrorsServiceMock, +} from '../../../../../src/core/public/mocks'; +import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; + import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies, fetchedNodes } from '../../public/application/store/actions'; +import { fetchedPolicies } from '../../public/application/store/actions'; import { indexLifecycleManagementStore } from '../../public/application/store'; import { EditPolicy } from '../../public/application/sections/edit_policy'; import { init as initHttp } from '../../public/application/services/http'; @@ -33,15 +40,17 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/application/store/selectors/lifecycle'; +} from '../../public/application/store/selectors'; -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); -initNotification({ - addDanger: () => {}, -}); +initHttp(axios.create({ adapter: axiosXhrAdapter })); +initUiMetric(usageCollectionPluginMock.createSetupContract()); +initNotification( + notificationServiceMock.createSetupContract().toasts, + fatalErrorsServiceMock.createSetupContract() +); let server; +let httpRequestsMockHelpers; let store; const policy = { phases: { @@ -70,9 +79,11 @@ for (let i = 0; i < 105; i++) { window.scrollTo = jest.fn(); window.TextEncoder = null; let component; -const activatePhase = (rendered, phase) => { +const activatePhase = async (rendered, phase) => { const testSubject = `enablePhaseSwitch-${phase}`; - findTestSubject(rendered, testSubject).simulate('click'); + await act(async () => { + await findTestSubject(rendered, testSubject).simulate('click'); + }); rendered.update(); }; const expectedErrorMessages = (rendered, expectedErrorMessages) => { @@ -120,16 +131,13 @@ describe('edit policy', () => { store = indexLifecycleManagementStore(); component = ( - {}} /> + {} }} getUrlForApp={() => {}} /> ); store.dispatch(fetchedPolicies(policies)); - server = sinon.fakeServer.create(); - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(policies), - ]); + ({ server, httpRequestsMockHelpers } = initHttpRequests()); + + httpRequestsMockHelpers.setPoliciesResponse(policies); }); describe('top level form', () => { test('should show error when trying to save empty form', () => { @@ -242,48 +250,53 @@ describe('edit policy', () => { }); }); describe('warm phase', () => { - test('should show number required error when trying to save empty warm phase', () => { + beforeEach(() => { + server.respondImmediately = true; + httpRequestsMockHelpers.setNodesListResponse({}); + }); + + test('should show number required error when trying to save empty warm phase', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', ''); save(rendered); expectedErrorMessages(rendered, [numberRequiredMessage]); }); - test('should allow 0 for phase timing', () => { + test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', 0); save(rendered); expectedErrorMessages(rendered, []); }); - test('should show positive number required error when trying to save warm phase with -1 for after', () => { + test('should show positive number required error when trying to save warm phase with -1 for after', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', -1); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); - test('should show positive number required error when trying to save warm phase with -1 for index priority', () => { + test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', 1); setPhaseIndexPriority(rendered, 'warm', -1); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); - test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', () => { + test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); findTestSubject(rendered, 'shrinkSwitch').simulate('click'); rendered.update(); setPhaseAfter(rendered, 'warm', 1); @@ -293,11 +306,11 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', () => { + test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', 1); findTestSubject(rendered, 'shrinkSwitch').simulate('click'); rendered.update(); @@ -307,11 +320,11 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', () => { + test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', 1); findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); rendered.update(); @@ -321,11 +334,11 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', () => { + test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', 1); findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); rendered.update(); @@ -335,43 +348,43 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show spinner for node attributes input when loading', () => { + test('should show spinner for node attributes input when loading', async () => { + server.respondImmediately = false; const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); - test('should show warning instead of node attributes input when none exist', () => { - store.dispatch(fetchedNodes({})); + test('should show warning instead of node attributes input when none exist', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); - test('should show node attributes input when attributes exist', () => { - store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] })); + test('should show node attributes input when attributes exist', async () => { + httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); - test('should show view node attributes link when attribute selected and show flyout when clicked', () => { - store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] })); + test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { + httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'warm'); + await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); @@ -388,61 +401,65 @@ describe('edit policy', () => { }); }); describe('cold phase', () => { - test('should allow 0 for phase timing', () => { + beforeEach(() => { + server.respondImmediately = true; + httpRequestsMockHelpers.setNodesListResponse({}); + }); + test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', 0); save(rendered); expectedErrorMessages(rendered, []); }); - test('should show positive number required error when trying to save cold phase with -1 for after', () => { + test('should show positive number required error when trying to save cold phase with -1 for after', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', -1); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); - test('should show spinner for node attributes input when loading', () => { + test('should show spinner for node attributes input when loading', async () => { + server.respondImmediately = false; const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); - test('should show warning instead of node attributes input when none exist', () => { - store.dispatch(fetchedNodes({})); + test('should show warning instead of node attributes input when none exist', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); - test('should show node attributes input when attributes exist', () => { - store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] })); + test('should show node attributes input when attributes exist', async () => { + httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); - test('should show view node attributes link when attribute selected and show flyout when clicked', () => { - store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] })); + test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { + httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); @@ -457,11 +474,11 @@ describe('edit policy', () => { rendered.update(); expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); }); - test('should show positive number required error when trying to save with -1 for index priority', () => { + test('should show positive number required error when trying to save with -1 for index priority', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'cold'); + await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', 1); setPhaseIndexPriority(rendered, 'cold', -1); save(rendered); @@ -469,20 +486,20 @@ describe('edit policy', () => { }); }); describe('delete phase', () => { - test('should allow 0 for phase timing', () => { + test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'delete'); + await activatePhase(rendered, 'delete'); setPhaseAfter(rendered, 'delete', 0); save(rendered); expectedErrorMessages(rendered, []); }); - test('should show positive number required error when trying to save delete phase with -1 for after', () => { + test('should show positive number required error when trying to save delete phase with -1 for after', async () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - activatePhase(rendered, 'delete'); + await activatePhase(rendered, 'delete'); setPhaseAfter(rendered, 'delete', -1); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts new file mode 100644 index 00000000000000..b5c941beef1818 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon, { SinonFakeServer } from 'sinon'; + +type HttpResponse = Record | any[]; + +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setPoliciesResponse = (response: HttpResponse = []) => { + server.respondWith('/api/index_lifecycle_management/policies', [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + + const setNodesListResponse = (response: HttpResponse = []) => { + server.respondWith('/api/index_lifecycle_management/nodes/list', [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + + return { + setPoliciesResponse, + setNodesListResponse, + }; +}; + +export const init = () => { + const server = sinon.fakeServer.create(); + + // Define default response for unhandled requests. + // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, + // and we can mock them all with a 200 instead of mocking each one individually. + server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); + + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + + return { + server, + httpRequestsMockHelpers, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js deleted file mode 100644 index 2284b9e39835ce..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { createDocLink } from '../../services/documentation'; - -export class LearnMoreLink extends React.PureComponent { - render() { - const { href, docPath, text } = this.props; - let url; - if (docPath) { - url = createDocLink(docPath); - } else { - url = href; - } - const content = text ? ( - text - ) : ( - - ); - return ( - - {content} - - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx new file mode 100644 index 00000000000000..623ff982438d7a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { createDocLink } from '../../services/documentation'; + +interface Props { + docPath: string; + text?: ReactNode; +} + +export const LearnMoreLink: React.FunctionComponent = ({ docPath, text }) => { + const content = text ? ( + text + ) : ( + + ); + return ( + + {content} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts similarity index 79% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts index 9138c6a30cfad0..4675ab46ee501c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { NodeAllocation } from './node_allocation.container'; +export { NodeAllocation } from './node_allocation'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js deleted file mode 100644 index 0ddfcbb940aa48..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; - -import { getNodeOptions } from '../../../../store/selectors'; -import { fetchNodes } from '../../../../store/actions'; -import { NodeAllocation as PresentationComponent } from './node_allocation'; - -export const NodeAllocation = connect( - (state) => ({ - nodeOptions: getNodeOptions(state), - }), - { - fetchNodes, - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js deleted file mode 100644 index 95c18787766883..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { EuiSelect, EuiButtonEmpty, EuiCallOut, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; - -import { PHASE_NODE_ATTRS } from '../../../../constants'; -import { LearnMoreLink } from '../../../components/learn_more_link'; -import { ErrableFormRow } from '../../form_errors'; - -const learnMoreLinks = ( - - - - - } - docPath="shards-allocation.html" - /> - -); - -export class NodeAllocation extends Component { - componentDidMount() { - this.props.fetchNodes(); - } - - render() { - const { - phase, - setPhaseData, - isShowingErrors, - phaseData, - showNodeDetailsFlyout, - nodeOptions, - errors, - } = this.props; - if (!nodeOptions) { - return ( - - - - - ); - } - if (!nodeOptions.length) { - return ( - - - } - color="warning" - > - - {learnMoreLinks} - - - - - ); - } - - return ( - - - { - setPhaseData(PHASE_NODE_ATTRS, e.target.value); - }} - /> - - {!!phaseData[PHASE_NODE_ATTRS] ? ( - showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])} - > - - - ) : ( -
- )} - {learnMoreLinks} - - - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx new file mode 100644 index 00000000000000..208f6b2aa61312 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiSelect, + EuiButtonEmpty, + EuiCallOut, + EuiSpacer, + EuiLoadingSpinner, + EuiButton, +} from '@elastic/eui'; + +import { PHASE_NODE_ATTRS } from '../../../../constants'; +import { LearnMoreLink } from '../../../components/learn_more_link'; +import { ErrableFormRow } from '../../form_errors'; +import { useLoadNodes } from '../../../../services/api'; + +interface Props { + phase: string; + setPhaseData: (dataKey: string, value: any) => void; + showNodeDetailsFlyout: (nodeAttrs: any) => void; + errors: any; + phaseData: any; + isShowingErrors: boolean; +} + +const learnMoreLink = ( + + + + } + docPath="modules-cluster.html#cluster-shard-allocation-settings" + /> + +); + +export const NodeAllocation: React.FunctionComponent = ({ + phase, + setPhaseData, + showNodeDetailsFlyout, + errors, + phaseData, + isShowingErrors, +}) => { + const { isLoading, data: nodes, error, sendRequest } = useLoadNodes(); + + if (isLoading) { + return ( + + + + + ); + } + + if (error) { + const { statusCode, message } = error; + return ( + + + } + color="danger" + > +

+ {message} ({statusCode}) +

+ + + +
+ + +
+ ); + } + + let nodeOptions = Object.keys(nodes).map((attrs) => ({ + text: `${attrs} (${nodes[attrs].length})`, + value: attrs, + })); + + nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); + if (nodeOptions.length) { + nodeOptions = [ + { + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.defaultNodeAllocation', { + defaultMessage: "Default allocation (don't use attributes)", + }), + value: '', + }, + ...nodeOptions, + ]; + } + if (!nodeOptions.length) { + return ( + + + } + color="warning" + > + + {learnMoreLink} + + + + + ); + } + + return ( + + + { + setPhaseData(PHASE_NODE_ATTRS, e.target.value); + }} + /> + + {!!phaseData[PHASE_NODE_ATTRS] ? ( + showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])} + > + + + ) : null} + {learnMoreLink} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx similarity index 57% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx index 28ebad209ad967..a3278b6c231b94 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx @@ -4,10 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { cloneElement, Children, Fragment } from 'react'; -import { EuiFormRow } from '@elastic/eui'; +import React, { cloneElement, Children, Fragment, ReactElement } from 'react'; +import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; -export const ErrableFormRow = ({ errorKey, isShowingErrors, errors, children, ...rest }) => { +type Props = EuiFormRowProps & { + errorKey: string; + isShowingErrors: boolean; + errors: Record; +}; + +export const ErrableFormRow: React.FunctionComponent = ({ + errorKey, + isShowingErrors, + errors, + children, + ...rest +}) => { return ( 0} @@ -16,7 +28,7 @@ export const ErrableFormRow = ({ errorKey, isShowingErrors, errors, children, .. > {Children.map(children, (child) => - cloneElement(child, { + cloneElement(child as ReactElement, { isInvalid: isShowingErrors && errors[errorKey].length > 0, }) )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 30c341baa61946..8838caa960b0c9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -21,9 +21,13 @@ interface GenericObject { [key: string]: any; } -export async function loadNodes() { - return await sendGet(`nodes/list`); -} +export const useLoadNodes = () => { + return useRequest({ + path: `nodes/list`, + method: 'get', + initialData: [], + }); +}; export async function loadNodeDetails(selectedNodeAttrs: string) { return await sendGet(`nodes/${selectedNodeAttrs}/details`); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts index 0b5f39a52c13fd..fb1a651b5f550e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts @@ -8,7 +8,6 @@ import { HttpSetup } from 'src/core/public'; import { UseRequestConfig, useRequest as _useRequest, - Error, } from '../../../../../../src/plugins/es_ui_shared/public'; interface GenericObject { @@ -43,6 +42,8 @@ export function sendDelete(path: string) { return _httpClient.delete(getFullPath(path)); } -export const useRequest = (config: UseRequestConfig) => { - return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) }); +export const useRequest = ( + config: UseRequestConfig +) => { + return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) }); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js index f2520abc7a4410..0b4026f019210b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js @@ -6,30 +6,12 @@ import { i18n } from '@kbn/i18n'; import { createAction } from 'redux-actions'; import { showApiError } from '../../services/api_errors'; -import { loadNodes, loadNodeDetails } from '../../services/api'; +import { loadNodeDetails } from '../../services/api'; import { SET_SELECTED_NODE_ATTRS } from '../../constants'; export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS); export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); -export const fetchedNodes = createAction('FETCHED_NODES'); -let fetchingNodes = false; -export const fetchNodes = () => async (dispatch) => { - try { - if (!fetchingNodes) { - fetchingNodes = true; - const nodes = await loadNodes(); - dispatch(fetchedNodes(nodes)); - } - } catch (err) { - const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage', { - defaultMessage: 'Error loading node attribute information', - }); - showApiError(err, title); - } finally { - fetchingNodes = false; - } -}; export const fetchedNodeDetails = createAction( 'FETCHED_NODE_DETAILS', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js index 443b257b6fb7ed..06d173e9901f8c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js @@ -6,7 +6,6 @@ import { handleActions } from 'redux-actions'; import { - fetchedNodes, setSelectedNodeAttrs, setSelectedPrimaryShardCount, setSelectedReplicaCount, @@ -24,13 +23,6 @@ const defaultState = { export const nodes = handleActions( { - [fetchedNodes](state, { payload: nodes }) { - return { - ...state, - isLoading: false, - nodes, - }; - }, [fetchedNodeDetails](state, { payload }) { const { selectedNodeAttrs, details } = payload; return { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js index 63d849217f59ed..561681bf7d93d9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js @@ -4,28 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createSelector } from 'reselect'; - export const getNodes = (state) => state.nodes.nodes; -export const getNodeOptions = createSelector([(state) => getNodes(state)], (nodes) => { - if (!nodes) { - return null; - } - - const options = Object.keys(nodes).map((attrs) => ({ - text: `${attrs} (${nodes[attrs].length})`, - value: attrs, - })); - - options.sort((a, b) => a.value.localeCompare(b.value)); - if (options.length) { - return [{ text: "Default allocation (don't use attributes)", value: '' }, ...options]; - } else { - return options; - } -}); - export const getSelectedPrimaryShardCount = (state) => state.nodes.selectedPrimaryShardCount; export const getSelectedReplicaCount = (state) => diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8218904f77df9e..c2f180f5268d48 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8207,7 +8207,6 @@ "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "ノード属性の詳細の読み込み中にエラーが発生しました", - "xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage": "ノード属性の情報の読み込み中にエラーが発生しました", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 21a42362bcdd3e..84c3eab8db9e76 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8209,7 +8209,6 @@ "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "加载节点属性详细信息时出错", - "xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage": "加载节点属性信息时出错", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位", From 042254f026bd77e4411d056c2af305588c8974ed Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 6 Aug 2020 09:17:20 -0700 Subject: [PATCH 12/31] [Ingest Manager] Update `dataset.*` to `data_stream.*` in package config SO attributes (#74414) * Update `dataset.*` to `data_stream.*` in full agent config yaml * Replace `dataset.*` with `data_stream.*` in package config saved object attributes --- .../common/services/config_to_yaml.ts | 1 + .../package_configs_to_agent_inputs.test.ts | 14 +++++------ .../package_configs_to_agent_inputs.ts | 4 ++-- .../common/services/package_to_config.test.ts | 24 ++++++++++--------- .../common/services/package_to_config.ts | 15 +++++------- .../common/types/models/agent_config.ts | 6 ++--- .../common/types/models/package_config.ts | 4 ++-- .../components/package_config_input_panel.tsx | 11 +++++---- .../services/validate_package_config.ts | 2 +- .../validate_package_config.ts.test.ts | 24 +++++++++---------- .../step_configure_package.tsx | 8 +++---- .../server/saved_objects/index.ts | 4 ++-- .../server/services/package_config.test.ts | 8 +++---- .../server/services/package_config.ts | 4 ++-- .../server/types/models/package_config.ts | 2 +- .../server/lib/hosts/mock.ts | 2 +- .../es_archives/fleet/agents/mappings.json | 4 ++-- .../es_archives/lists/mappings.json | 4 ++-- .../canvas_disallowed_url/mappings.json | 4 ++-- .../es_archives/export_rule/mappings.json | 4 ++-- .../apps/endpoint/policy_details.ts | 2 +- 21 files changed, 76 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts index 1fb6fead454ef8..e2e6393738d1f2 100644 --- a/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts +++ b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts @@ -10,6 +10,7 @@ const CONFIG_KEYS_ORDER = [ 'id', 'name', 'revision', + 'dataset', 'type', 'outputs', 'agent', diff --git a/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts index a4d87f54b0915a..d6c09f058ab85b 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts @@ -39,7 +39,7 @@ describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { { id: 'test-logs-foo', enabled: true, - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, vars: { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, @@ -52,7 +52,7 @@ describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { { id: 'test-logs-bar', enabled: true, - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, vars: { barVar: { value: 'bar-value' }, barVar2: { value: [1, 2] }, @@ -118,7 +118,7 @@ describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { id: 'some-uuid', name: 'mock-package-config', type: 'test-logs', - dataset: { namespace: 'default' }, + data_stream: { namespace: 'default' }, use_output: 'default', meta: { package: { @@ -129,13 +129,13 @@ describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { streams: [ { id: 'test-logs-foo', - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, fooKey: 'fooValue1', fooKey2: ['fooValue2'], }, { id: 'test-logs-bar', - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, }, ], }, @@ -160,12 +160,12 @@ describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { id: 'some-uuid', name: 'mock-package-config', type: 'test-logs', - dataset: { namespace: 'default' }, + data_stream: { namespace: 'default' }, use_output: 'default', streams: [ { id: 'test-logs-foo', - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, fooKey: 'fooValue1', fooKey2: ['fooValue2'], }, diff --git a/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts index 64ba6b8a52b57b..b94fc39e0567cc 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts @@ -24,7 +24,7 @@ export const storedPackageConfigsToAgentInputs = ( id: packageConfig.id || packageConfig.name, name: packageConfig.name, type: input.type, - dataset: { + data_stream: { namespace: packageConfig.namespace || 'default', }, use_output: DEFAULT_OUTPUT.name, @@ -37,7 +37,7 @@ export const storedPackageConfigsToAgentInputs = ( .map((stream) => { const fullStream: FullAgentConfigInputStream = { id: stream.id, - dataset: stream.dataset, + data_stream: stream.data_stream, ...stream.compiled_stream, ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { acc[key] = value; diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index e0cd32df1535eb..1f4cd43247be17 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -83,14 +83,16 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'foo', enabled: true, - streams: [{ id: 'foo-foo', enabled: true, dataset: { name: 'foo', type: 'logs' } }], + streams: [ + { id: 'foo-foo', enabled: true, data_stream: { dataset: 'foo', type: 'logs' } }, + ], }, { type: 'bar', enabled: true, streams: [ - { id: 'bar-bar', enabled: true, dataset: { name: 'bar', type: 'logs' } }, - { id: 'bar-bar2', enabled: true, dataset: { name: 'bar2', type: 'logs' } }, + { id: 'bar-bar', enabled: true, data_stream: { dataset: 'bar', type: 'logs' } }, + { id: 'bar-bar2', enabled: true, data_stream: { dataset: 'bar2', type: 'logs' } }, ], }, ]); @@ -141,7 +143,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'foo-foo', enabled: true, - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, vars: { 'var-name': { value: 'foo-var-value' } }, }, ], @@ -153,13 +155,13 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'bar-bar', enabled: true, - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, vars: { 'var-name': { type: 'text', value: 'bar-var-value' } }, }, { id: 'bar-bar2', enabled: true, - dataset: { name: 'bar2', type: 'logs' }, + data_stream: { dataset: 'bar2', type: 'logs' }, vars: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, }, ], @@ -257,7 +259,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'foo-foo', enabled: true, - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, vars: { 'var-name': { value: 'foo-var-value' }, }, @@ -275,7 +277,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'bar-bar', enabled: true, - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, vars: { 'var-name': { value: 'bar-var-value' }, }, @@ -283,7 +285,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'bar-bar2', enabled: true, - dataset: { name: 'bar2', type: 'logs' }, + data_stream: { dataset: 'bar2', type: 'logs' }, vars: { 'var-name': { value: 'bar2-var-value' }, }, @@ -297,7 +299,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'with-disabled-streams-disabled', enabled: false, - dataset: { name: 'disabled', type: 'logs' }, + data_stream: { dataset: 'disabled', type: 'logs' }, vars: { 'var-name': { value: [] }, }, @@ -305,7 +307,7 @@ describe('Ingest Manager - packageToConfig', () => { { id: 'with-disabled-streams-disabled2', enabled: false, - dataset: { name: 'disabled2', type: 'logs' }, + data_stream: { dataset: 'disabled2', type: 'logs' }, }, ], }, diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts index 5957267c7304c2..184b44cb9e5307 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts @@ -19,17 +19,17 @@ import { const getStreamsForInputType = ( inputType: string, packageInfo: PackageInfo -): Array => { - const streams: Array = []; +): Array => { + const streams: Array = []; (packageInfo.datasets || []).forEach((dataset) => { (dataset.streams || []).forEach((stream) => { if (stream.input === inputType) { streams.push({ ...stream, - dataset: { + data_stream: { type: dataset.type, - name: dataset.name, + dataset: dataset.name, }, }); } @@ -76,12 +76,9 @@ export const packageToPackageConfigInputs = (packageInfo: PackageInfo): PackageC packageInfo ).map((packageStream) => { const stream: PackageConfigInputStream = { - id: `${packageInput.type}-${packageStream.dataset.name}`, + id: `${packageInput.type}-${packageStream.data_stream.dataset}`, enabled: packageStream.enabled === false ? false : true, - dataset: { - name: packageStream.dataset.name, - type: packageStream.dataset.type, - }, + data_stream: packageStream.data_stream, }; if (packageStream.vars && packageStream.vars.length) { stream.vars = packageStream.vars.reduce(varsReducer, {}); diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 00ba51fc1843a9..cdaea1cc5f9a4b 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -32,8 +32,8 @@ export type AgentConfigSOAttributes = Omit; export interface FullAgentConfigInputStream { id: string; - dataset: { - name: string; + data_stream: { + dataset: string; type: string; }; [key: string]: any; @@ -43,7 +43,7 @@ export interface FullAgentConfigInput { id: string; name: string; type: string; - dataset: { namespace: string }; + data_stream: { namespace: string }; use_output: string; meta?: { package?: Pick; diff --git a/x-pack/plugins/ingest_manager/common/types/models/package_config.ts b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts index 0ff56e6d05d370..635afbd47850ed 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/package_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts @@ -20,8 +20,8 @@ export type PackageConfigConfigRecord = Record, + packageInputStreams: Array, packageConfigInput: PackageConfigInput ): boolean => { return ( @@ -52,7 +52,7 @@ const shouldShowStreamsByDefault = ( hasInvalidButRequiredVar( stream.vars, packageConfigInput.streams.find( - (pkgStream) => stream.dataset.name === pkgStream.dataset.name + (pkgStream) => stream.data_stream.dataset === pkgStream.data_stream.dataset )?.vars ) ) @@ -62,7 +62,7 @@ const shouldShowStreamsByDefault = ( export const PackageConfigInputPanel: React.FunctionComponent<{ packageInput: RegistryInput; - packageInputStreams: Array; + packageInputStreams: Array; packageConfigInput: PackageConfigInput; updatePackageConfigInput: (updatedInput: Partial) => void; inputValidationResults: PackageConfigInputValidationResults; @@ -90,7 +90,7 @@ export const PackageConfigInputPanel: React.FunctionComponent<{ return { packageInputStream, packageConfigInputStream: packageConfigInput.streams.find( - (stream) => stream.dataset.name === packageInputStream.dataset.name + (stream) => stream.data_stream.dataset === packageInputStream.data_stream.dataset ), }; }) @@ -201,7 +201,8 @@ export const PackageConfigInputPanel: React.FunctionComponent<{ updatedStream: Partial ) => { const indexOfUpdatedStream = packageConfigInput.streams.findIndex( - (stream) => stream.dataset.name === packageInputStream.dataset.name + (stream) => + stream.data_stream.dataset === packageInputStream.data_stream.dataset ); const newStreams = [...packageConfigInput.streams]; newStreams[indexOfUpdatedStream] = { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts index bd9d216ca969a4..0514ad574a8cdb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts @@ -134,7 +134,7 @@ export const validatePackageConfig = ( if (stream.vars) { const streamVarsByName = ( ( - registryStreamsByDataset[stream.dataset.name].find( + registryStreamsByDataset[stream.data_stream.dataset].find( (registryStream) => registryStream.input === input.type ) || {} ).vars || [] diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts index 41d46f03dca233..47874525b8a5a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts @@ -159,7 +159,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'foo-foo', - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, enabled: true, vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, @@ -175,13 +175,13 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'bar-bar', - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, enabled: true, vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, { id: 'bar-bar2', - dataset: { name: 'bar2', type: 'logs' }, + data_stream: { dataset: 'bar2', type: 'logs' }, enabled: true, vars: { 'var-name': { value: undefined, type: 'text' } }, }, @@ -198,13 +198,13 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'with-disabled-streams-disabled', - dataset: { name: 'disabled', type: 'logs' }, + data_stream: { dataset: 'disabled', type: 'logs' }, enabled: false, vars: { 'var-name': { value: undefined, type: 'text' } }, }, { id: 'with-disabled-streams-disabled-without-vars', - dataset: { name: 'disabled2', type: 'logs' }, + data_stream: { dataset: 'disabled2', type: 'logs' }, enabled: false, }, ], @@ -218,7 +218,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'with-no-stream-vars-bar', - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, enabled: true, }, ], @@ -241,7 +241,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'foo-foo', - dataset: { name: 'foo', type: 'logs' }, + data_stream: { dataset: 'foo', type: 'logs' }, enabled: true, vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, }, @@ -257,13 +257,13 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'bar-bar', - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, enabled: true, vars: { 'var-name': { value: ' \n\n', type: 'yaml' } }, }, { id: 'bar-bar2', - dataset: { name: 'bar2', type: 'logs' }, + data_stream: { dataset: 'bar2', type: 'logs' }, enabled: true, vars: { 'var-name': { value: undefined, type: 'text' } }, }, @@ -280,7 +280,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'with-disabled-streams-disabled', - dataset: { name: 'disabled', type: 'logs' }, + data_stream: { dataset: 'disabled', type: 'logs' }, enabled: false, vars: { 'var-name': { @@ -291,7 +291,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { }, { id: 'with-disabled-streams-disabled-without-vars', - dataset: { name: 'disabled2', type: 'logs' }, + data_stream: { dataset: 'disabled2', type: 'logs' }, enabled: false, }, ], @@ -305,7 +305,7 @@ describe('Ingest Manager - validatePackageConfig()', () => { streams: [ { id: 'with-no-stream-vars-bar', - dataset: { name: 'bar', type: 'logs' }, + data_stream: { dataset: 'bar', type: 'logs' }, enabled: true, }, ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx index 380a03e15695bb..a41d4d72db34ca 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx @@ -14,16 +14,16 @@ import { CreatePackageConfigFrom } from './types'; const findStreamsForInputType = ( inputType: string, packageInfo: PackageInfo -): Array => { - const streams: Array = []; +): Array => { + const streams: Array = []; (packageInfo.datasets || []).forEach((dataset) => { (dataset.streams || []).forEach((stream) => { if (stream.input === inputType) { streams.push({ ...stream, - dataset: { - name: dataset.name, + data_stream: { + dataset: dataset.name, }, }); } diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index aa2b73194067ac..eca2711363c854 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -211,9 +211,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { id: { type: 'keyword' }, enabled: { type: 'boolean' }, - dataset: { + data_stream: { properties: { - name: { type: 'keyword' }, + dataset: { type: 'keyword' }, type: { type: 'keyword' }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.test.ts b/x-pack/plugins/ingest_manager/server/services/package_config.test.ts index e86e2608e252d8..28aa0d773d75b7 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.test.ts @@ -65,7 +65,7 @@ describe('Package config service', () => { streams: [ { id: 'dataset01', - dataset: { name: 'package.dataset1', type: 'logs' }, + data_stream: { dataset: 'package.dataset1', type: 'logs' }, enabled: true, vars: { paths: { @@ -85,7 +85,7 @@ describe('Package config service', () => { streams: [ { id: 'dataset01', - dataset: { name: 'package.dataset1', type: 'logs' }, + data_stream: { dataset: 'package.dataset1', type: 'logs' }, enabled: true, vars: { paths: { @@ -131,7 +131,7 @@ describe('Package config service', () => { streams: [ { id: 'dataset01', - dataset: { name: 'package.dataset1', type: 'logs' }, + data_stream: { dataset: 'package.dataset1', type: 'logs' }, enabled: true, }, ], @@ -151,7 +151,7 @@ describe('Package config service', () => { streams: [ { id: 'dataset01', - dataset: { name: 'package.dataset1', type: 'logs' }, + data_stream: { dataset: 'package.dataset1', type: 'logs' }, enabled: true, compiled_stream: { metricset: ['dataset1'], diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index a369aa5c41cd46..665c08316588c0 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -379,14 +379,14 @@ async function _assignPackageStreamToStream( if (!stream.enabled) { return { ...stream, compiled_stream: undefined }; } - const datasetPath = getDataset(stream.dataset.name); + const datasetPath = getDataset(stream.data_stream.dataset); const packageDatasets = pkgInfo.datasets; if (!packageDatasets) { throw new Error('Stream template not found, no datasets'); } const packageDataset = packageDatasets.find( - (pkgDataset) => pkgDataset.name === stream.dataset.name + (pkgDataset) => pkgDataset.name === stream.data_stream.dataset ); if (!packageDataset) { throw new Error(`Stream template not found, unable to find dataset ${datasetPath}`); diff --git a/x-pack/plugins/ingest_manager/server/types/models/package_config.ts b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts index 0823ccd85a32b3..9b7ffb4f781750 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts @@ -45,7 +45,7 @@ const PackageConfigBaseSchema = { schema.object({ id: schema.string(), enabled: schema.boolean(), - dataset: schema.object({ name: schema.string(), type: schema.string() }), + data_stream: schema.object({ dataset: schema.string(), type: schema.string() }), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 44767563c6b754..97aa68c0f9bbf1 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -588,7 +588,7 @@ export const mockEndpointMetadata = { type: 'endpoint', version: '7.9.0-SNAPSHOT', }, - dataset: { name: 'endpoint.metadata', namespace: 'default', type: 'metrics' }, + data_stream: { dataset: 'endpoint.metadata', namespace: 'default', type: 'metrics' }, ecs: { version: '1.5.0' }, elastic: { agent: { id: '' } }, event: { diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 23b404a53703f4..12d3be3e2a9710 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -1870,9 +1870,9 @@ "config": { "type": "flattened" }, - "dataset": { + "data_stream": { "properties": { - "name": { + "dataset": { "type": "keyword" }, "type": { diff --git a/x-pack/test/functional/es_archives/lists/mappings.json b/x-pack/test/functional/es_archives/lists/mappings.json index 3b4d915cc1ca5a..ba4e1b276d45ef 100644 --- a/x-pack/test/functional/es_archives/lists/mappings.json +++ b/x-pack/test/functional/es_archives/lists/mappings.json @@ -1310,9 +1310,9 @@ "config": { "type": "flattened" }, - "dataset": { + "data_stream": { "properties": { - "name": { + "dataset": { "type": "keyword" }, "type": { diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json index 3519103d068148..2380154277e552 100644 --- a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json @@ -1246,9 +1246,9 @@ "config": { "type": "flattened" }, - "dataset": { + "data_stream": { "properties": { - "name": { + "dataset": { "type": "keyword" }, "type": { diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json index bb63d29503663a..249b03981386d3 100644 --- a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json @@ -1320,9 +1320,9 @@ "config": { "type": "flattened" }, - "dataset": { + "data_stream": { "properties": { - "name": { + "dataset": { "type": "keyword" }, "type": { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index d4947222a6cc0d..02f893029f8191 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -108,7 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { inputs: [ { id: policyInfo.packageConfig.id, - dataset: { namespace: 'default' }, + data_stream: { namespace: 'default' }, name: 'Protect East Coast', meta: { package: { From fa2251dd31ff6292e7f99aec1f42d3b427d7d39a Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 6 Aug 2020 18:52:21 +0200 Subject: [PATCH 13/31] [Lens] Add functional tests on chart transitions and pie chart (#74083) --- .../change_indexpattern.tsx | 2 +- .../pie_visualization/pie_visualization.tsx | 4 + .../xy_visualization/xy_config_panel.test.tsx | 6 +- .../xy_visualization/xy_config_panel.tsx | 2 +- .../api_integration/apis/lens/telemetry.ts | 3 +- x-pack/test/functional/apps/lens/dashboard.ts | 64 ++++++ x-pack/test/functional/apps/lens/index.ts | 1 + .../test/functional/apps/lens/smokescreen.ts | 203 +++++++++--------- .../es_archives/lens/basic/data.json.gz | Bin 4183 -> 4623 bytes .../test/functional/page_objects/lens_page.ts | 76 ++++++- 10 files changed, 255 insertions(+), 106 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/dashboard.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 5e2fe9d7bbc14b..a62db353e2bafa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -57,7 +57,7 @@ export function ChangeIndexPattern({ panelPaddingSize="s" ownFocus > -
+
{i18n.translate('xpack.lens.indexPattern.changeIndexPatternTitle', { defaultMessage: 'Change index pattern', diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx index 369ab28293fbc8..5a68516db6aa3f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx @@ -122,6 +122,7 @@ export const pieVisualization: Visualization { ); const options = component - .find('[data-test-subj="lnsXY_seriesType"]') + .find(EuiButtonGroup) .first() .prop('options') as EuiButtonGroupProps['options']; @@ -79,7 +79,7 @@ describe('XY Config panels', () => { ); const options = component - .find('[data-test-subj="lnsXY_seriesType"]') + .find(EuiButtonGroup) .first() .prop('options') as EuiButtonGroupProps['options']; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 6d5bc7808a678f..e4bc6de5cc68ae 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -95,13 +95,13 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { })} name="chartType" className="eui-displayInlineBlock" - data-test-subj="lnsXY_seriesType" options={visualizationTypes .filter((t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) .map((t) => ({ id: t.id, label: t.label, iconType: t.icon || 'empty', + 'data-test-subj': `lnsXY_seriesType-${t.id}`, }))} idSelected={layer.seriesType} onChange={(seriesType) => { diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 2c05b02cf470f5..0ae4753cd2967d 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -191,8 +191,9 @@ export default ({ getService }: FtrProviderContext) => { expect(results.saved_overall).to.eql({ lnsMetric: 1, bar_stacked: 1, + lnsPie: 1, }); - expect(results.saved_overall_total).to.eql(2); + expect(results.saved_overall_total).to.eql(3); await esArchiver.unload('lens/basic'); }); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts new file mode 100644 index 00000000000000..ccf2f88a9d0ed1 --- /dev/null +++ b/x-pack/test/functional/apps/lens/dashboard.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['header', 'common', 'dashboard', 'timePicker', 'lens']); + + const find = getService('find'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const elasticChart = getService('elasticChart'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + + async function clickInChart(x: number, y: number) { + const el = await elasticChart.getCanvas(); + await browser.getActions().move({ x, y, origin: el._webElement }).click().perform(); + } + + describe('lens dashboard tests', () => { + it('metric should be embeddable', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens'); + await find.clickByButtonText('Artistpreviouslyknownaslens'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + }); + + it('should be able to add filters/timerange by clicking in XYChart', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await clickInChart(5, 5); // hardcoded position of bar + + await retry.try(async () => { + await testSubjects.click('applyFiltersPopoverButton'); + await testSubjects.missingOrFail('applyFiltersPopoverButton'); + }); + + await PageObjects.lens.assertExactText( + '[data-test-subj="embeddablePanelHeading-lnsXYvis"]', + 'lnsXYvis' + ); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000'); + expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000'); + const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); + expect(hasIpFilter).to.be(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index b17b7d856841c9..f2dcf28c017433 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -28,6 +28,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./lens_reporting')); }); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 1e936361610678..77b9aa1e25edd8 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -8,115 +8,20 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects([ - 'header', - 'common', - 'visualize', - 'dashboard', - 'header', - 'timePicker', - 'lens', - ]); + const PageObjects = getPageObjects(['visualize', 'lens']); const find = getService('find'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const elasticChart = getService('elasticChart'); - const browser = getService('browser'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const filterBar = getService('filterBar'); const listingTable = getService('listingTable'); - async function assertExpectedMetric(metricCount: string = '19,986') { - await PageObjects.lens.assertExactText( - '[data-test-subj="lns_metric_title"]', - 'Maximum of bytes' - ); - await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount); - } - - async function assertExpectedTable() { - await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] thead .euiTableCellContent__text', - 'Maximum of bytes' - ); - await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]', - '19,986' - ); - } - - async function assertExpectedChart() { - await PageObjects.lens.assertExactText( - '[data-test-subj="embeddablePanelHeading-lnsXYvis"]', - 'lnsXYvis' - ); - } - - async function assertExpectedTimerange() { - const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000'); - expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000'); - } - - async function clickOnBarHistogram() { - const el = await elasticChart.getCanvas(); - await browser.getActions().move({ x: 5, y: 5, origin: el._webElement }).click().perform(); - } - describe('lens smokescreen tests', () => { it('should allow editing saved visualizations', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); - }); - - it('metric should be embeddable in dashboards', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens'); - await find.clickByButtonText('Artistpreviouslyknownaslens'); - await dashboardAddPanel.closeAddPanel(); - await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }); - it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); - await find.clickByButtonText('lnsXYvis'); - await dashboardAddPanel.closeAddPanel(); - await PageObjects.lens.goToTimeRange(); - await clickOnBarHistogram(); - - await retry.try(async () => { - await testSubjects.click('applyFiltersPopoverButton'); - await testSubjects.missingOrFail('applyFiltersPopoverButton'); - }); - - await assertExpectedChart(); - await assertExpectedTimerange(); - const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); - expect(hasIpFilter).to.be(true); - }); - - it('should allow seamless transition to and from table view', async () => { - await PageObjects.visualize.gotoVisualizationLandingPage(); - await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); - await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); - await PageObjects.lens.goToTimeRange(); - await assertExpectedMetric(); - await PageObjects.lens.switchToVisualization('lnsDatatable'); - await assertExpectedTable(); - await PageObjects.lens.switchToVisualization('lnsMetric'); - await assertExpectedMetric(); - }); - - it('should allow creation of lens visualizations', async () => { + it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -165,6 +70,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + it('should allow seamless transition to and from table view', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Maximum of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('19,986'); + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + }); + it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -190,5 +108,94 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getLayerCount()).to.eql(2); }); + + it('should allow transition from line chart to donut chart and to bar chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + expect(await PageObjects.lens.hasChartSwitchWarning('donut')).to.eql(true); + await PageObjects.lens.switchToVisualization('donut'); + + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + + expect(await PageObjects.lens.hasChartSwitchWarning('bar')).to.eql(false); + await PageObjects.lens.switchToVisualization('bar'); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should allow seamless transition from bar chart to line chart using layer chart switch', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchLayerSeriesType('line'); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_splitDimensionPanel')).to.eql( + 'Top values of ip' + ); + }); + + it('should allow seamless transition from pie chart to treemap chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsPieVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis'); + await PageObjects.lens.goToTimeRange(); + expect(await PageObjects.lens.hasChartSwitchWarning('treemap')).to.eql(false); + await PageObjects.lens.switchToVisualization('treemap'); + expect( + await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 0) + ).to.eql('Top values of geo.dest'); + expect( + await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 1) + ).to.eql('Top values of geo.src'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should allow creating a pie chart and switching to datatable', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie'); + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('@timestamp per 3 hours'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2015-09-20 00:00'); + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.eql('Average of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('6,011.351'); + }); }); } diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz index 4ed7c29f7391e4b01ea2737a318cb533d72bf63a..ddf4a27289dffdaf1c605b0db01cfa9554a2b209 100644 GIT binary patch literal 4623 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 diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 79548db0e26301..bed0e3a159e23f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -176,9 +176,26 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont */ async hasChartSwitchWarning(subVisualizationId: string) { await this.openChartSwitchPopover(); - const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`); - return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element); + return await find.descendantExistsByCssSelector( + '.euiKeyPadMenuItem__betaBadgeWrapper', + element + ); + }, + + /** + * Uses the Lens layer switcher to switch seriesType for xy charts. + * + * @param subVisualizationId - the ID of the sub-visualization to switch to, such as + * line, + */ + async switchLayerSeriesType(seriesType: string) { + await retry.try(async () => { + await testSubjects.click('lns_layer_settings'); + await testSubjects.exists(`lnsXY_seriesType-${seriesType}`); + }); + + return await testSubjects.click(`lnsXY_seriesType-${seriesType}`); }, /** @@ -205,5 +222,60 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('lnsApp_saveAndReturnButton'); }, + /** + * Gets label of dimension trigger in dimension panel + * + * @param dimension - the selector of the dimension + */ + async getDimensionTriggerText(dimension: string, index = 0) { + const dimensionElements = await testSubjects.findAll(dimension); + const trigger = await testSubjects.findDescendant( + 'lns-dimensionTrigger', + dimensionElements[index] + ); + return await trigger.getVisibleText(); + }, + + /** + * Gets text of the specified datatable header cell + * + * @param index - index of th element in datatable + */ + async getDatatableHeaderText(index = 0) { + return find + .byCssSelector( + `[data-test-subj="lnsDataTable"] thead th:nth-child(${ + index + 1 + }) .euiTableCellContent__text` + ) + .then((el) => el.getVisibleText()); + }, + + /** + * Gets text of the specified datatable cell + * + * @param rowIndex - index of row of the cell + * @param colIndex - index of column of the cell + */ + async getDatatableCellText(rowIndex = 0, colIndex = 0) { + return find + .byCssSelector( + `[data-test-subj="lnsDataTable"] tr:nth-child(${rowIndex + 1}) td:nth-child(${ + colIndex + 1 + })` + ) + .then((el) => el.getVisibleText()); + }, + + /** + * Asserts that metric has expected title and count + * + * @param title - expected title + * @param count - expected count of metric + */ + async assertMetric(title: string, count: string) { + await this.assertExactText('[data-test-subj="lns_metric_title"]', title); + await this.assertExactText('[data-test-subj="lns_metric_value"]', count); + }, }); } From dad5c72a0c2313b3dde78d10817c05917cbda5d0 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 6 Aug 2020 13:24:54 -0400 Subject: [PATCH 14/31] [Fix] Lose OriginatingApp Connection on Save After Create new (#74420) Fixed typo created in #72725 which caused the originatingApp connection not to be lost properly after Create new --- .../application/utils/get_top_nav_config.tsx | 2 +- .../dashboard/edit_embeddable_redirects.js | 19 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +--- .../dashboard_mode/dashboard_empty_screen.js | 18 ++++++++++++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 392168a5300879..da9ba66a914ddf 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -114,7 +114,7 @@ export const getTopNavConfig = ( application.navigateToApp(originatingApp); } } else { - if (setOriginatingApp && originatingApp && savedVis.copyOnSave) { + if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); } chrome.docTitle.change(savedVis.lastSavedTitle); diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.js b/test/functional/apps/dashboard/edit_embeddable_redirects.js index 6d3d43890a9620..fcc504ea24f312 100644 --- a/test/functional/apps/dashboard/edit_embeddable_redirects.js +++ b/test/functional/apps/dashboard/edit_embeddable_redirects.js @@ -21,8 +21,10 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); describe('edit embeddable redirects', () => { before(async () => { @@ -81,6 +83,23 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); + await PageObjects.visualize.linkedToOriginatingApp(); + await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, { + saveAsNew: true, + redirectToOrigin: false, + }); + await PageObjects.visualize.notLinkedToOriginatingApp(); + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('loses originatingApp connection after first save when redirectToOrigin is false', async () => { + const newTitle = 'test create panel originatingApp'; + await PageObjects.dashboard.loadSavedDashboard('few panels'); + await PageObjects.dashboard.switchToEditMode(); + await testSubjects.exists('dashboardAddNewPanelButton'); + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, { saveAsNew: true, redirectToOrigin: false, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4a6dbd4a91fbfe..ffab84a51a2298 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -336,9 +336,7 @@ export function App({ ...s, isSaveModalVisible: false, originatingApp: - saveProps.newCopyOnSave && !saveProps.returnToOrigin - ? undefined - : currentOriginatingApp, + newlyCreated && !saveProps.returnToOrigin ? undefined : currentOriginatingApp, persistedDoc: newDoc, lastKnownDoc: newDoc, })); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index 62e07a08d17621..bd35374643e9b8 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -27,7 +27,7 @@ export default function ({ getPageObjects, getService }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); - async function createAndAddLens(title) { + async function createAndAddLens(title, saveAsNew = false, redirectToOrigin = true) { log.debug(`createAndAddLens(${title})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { @@ -52,7 +52,7 @@ export default function ({ getPageObjects, getService }) { operation: 'terms', field: 'ip', }); - await PageObjects.lens.save(title, false, true); + await PageObjects.lens.save(title, saveAsNew, redirectToOrigin); } it('adds Lens visualization to empty dashboard', async () => { @@ -100,6 +100,8 @@ export default function ({ getPageObjects, getService }) { }); it('loses originatingApp connection after save as when redirectToOrigin is false', async () => { + await PageObjects.dashboard.saveDashboard('empty dashboard test'); + await PageObjects.dashboard.switchToEditMode(); const newTitle = 'wowee, my title just got cooler again'; await PageObjects.dashboard.waitForRenderComplete(); await dashboardPanelActions.openContextMenu(); @@ -108,5 +110,17 @@ export default function ({ getPageObjects, getService }) { await PageObjects.lens.notLinkedToOriginatingApp(); await PageObjects.common.navigateToApp('dashboard'); }); + + it('loses originatingApp connection after first save when redirectToOrigin is false', async () => { + const title = 'non-dashboard Test Lens'; + await PageObjects.dashboard.loadSavedDashboard('empty dashboard test'); + await PageObjects.dashboard.switchToEditMode(); + await testSubjects.exists('dashboardAddNewPanelButton'); + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await createAndAddLens(title, false, false); + await PageObjects.lens.notLinkedToOriginatingApp(); + await PageObjects.common.navigateToApp('dashboard'); + }); }); } From 79713b9b71bdac049d1dbc754bb2ee1a3861e116 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 6 Aug 2020 10:57:29 -0700 Subject: [PATCH 15/31] [browserslist] remove user-agent sniffing for IE support hint (#74464) Co-authored-by: spalger --- package.json | 2 - packages/kbn-optimizer/README.md | 4 +- .../server/http/base_path_proxy_server.ts | 36 +----------------- yarn.lock | 38 ++++--------------- 4 files changed, 11 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index aaa7ae7ee46846..fa34fc3d0936a9 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,6 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", - "browserslist-useragent": "^3.0.2", "cache-loader": "^4.1.0", "chalk": "^2.4.2", "check-disk-space": "^2.1.0", @@ -319,7 +318,6 @@ "@types/babel__core": "^7.1.2", "@types/bluebird": "^3.1.1", "@types/boom": "^7.2.0", - "@types/browserslist-useragent": "^3.0.0", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.10", "@types/chromedriver": "^81.0.0", diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md index 5d5c5e3b6eb748..13be836f0ea885 100644 --- a/packages/kbn-optimizer/README.md +++ b/packages/kbn-optimizer/README.md @@ -10,9 +10,9 @@ The [Webpack config][WebpackConfig] is designed to provide the majority of what Source maps are enabled except when building the distributable. They show the code actually being executed by the browser to strike a balance between debuggability and performance. They are not configurable at this time but will be configurable once we have a developer configuration solution that doesn't rely on the server (see [#55656](https://github.com/elastic/kibana/issues/55656)). -### IE Support +### Browser Support -To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support older browsers like IE in development you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing. +To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support all browsers that we support with the distributable you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing. ## Running the optimizer diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index eccc9d013176cf..acb83962bd457c 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -22,10 +22,9 @@ import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; import apm from 'elastic-apm-node'; import { ByteSizeValue } from '@kbn/config-schema'; -import { Server, Request, ResponseToolkit } from 'hapi'; +import { Server, Request } from 'hapi'; import HapiProxy from 'h2o2'; import { sampleSize } from 'lodash'; -import BrowserslistUserAgent from 'browserslist-useragent'; import * as Rx from 'rxjs'; import { take } from 'rxjs/operators'; @@ -41,34 +40,6 @@ export interface BasePathProxyServerOptions { delayUntil: () => Rx.Observable; } -// Before we proxy request to a target port we may want to wait until some -// condition is met (e.g. until target listener is ready). -const checkForBrowserCompat = (log: Logger) => async (request: Request, h: ResponseToolkit) => { - if (!request.headers['user-agent'] || process.env.BROWSERSLIST_ENV === 'production') { - return h.continue; - } - - const matches = BrowserslistUserAgent.matchesUA(request.headers['user-agent'], { - env: 'dev', - allowHigherVersions: true, - ignoreMinor: true, - ignorePath: true, - }); - - if (!matches) { - log.warn(` - Request with user-agent [${request.headers['user-agent']}] - seems like it is coming from a browser that is not supported by the dev browserlist. - - Please run Kibana with the environment variable BROWSERSLIST_ENV=production to enable - support for all production browsers (like IE). - - `); - } - - return h.continue; -}; - export class BasePathProxyServer { private server?: Server; private httpsAgent?: HttpsAgent; @@ -155,9 +126,6 @@ export class BasePathProxyServer { }, method: 'GET', path: '/', - options: { - pre: [checkForBrowserCompat(this.log)], - }, }); this.server.route({ @@ -175,7 +143,6 @@ export class BasePathProxyServer { method: '*', options: { pre: [ - checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { @@ -210,7 +177,6 @@ export class BasePathProxyServer { method: '*', options: { pre: [ - checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { diff --git a/yarn.lock b/yarn.lock index 7aff34fab23ceb..f17418f07c5cc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4306,11 +4306,6 @@ resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.0.tgz#19c36cbb5811a7493f0f2e37f31d42b28df1abc1" integrity sha512-HonbGsHFbskh9zRAzA6tabcw18mCOsSEOL2ibGAuVqk6e7nElcRmWO5L4UfIHpDbWBWw+eZYFdsQ1+MEGgpcVA== -"@types/browserslist-useragent@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" - integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== - "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -8601,15 +8596,6 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist-useragent@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c" - integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A== - dependencies: - browserslist "^4.6.6" - semver "^6.3.0" - useragent "^2.3.0" - browserslist@4.6.6: version "4.6.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" @@ -8629,7 +8615,7 @@ browserslist@^4.12.0: node-releases "^1.1.53" pkg-up "^2.0.0" -browserslist@^4.6.6, browserslist@^4.8.3: +browserslist@^4.8.3: version "4.8.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.5.tgz#691af4e327ac877b25e7a3f7ee869c4ef36cdea3" integrity sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg== @@ -29564,13 +29550,6 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.0.x, tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmp@0.1.0, tmp@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" @@ -29585,6 +29564,13 @@ tmp@^0.0.29: dependencies: os-tmpdir "~1.0.1" +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -30711,14 +30697,6 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" - integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== - dependencies: - lru-cache "4.1.x" - tmp "0.0.x" - utif@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/utif/-/utif-2.0.1.tgz#9e1582d9bbd20011a6588548ed3266298e711759" From b71b9c249adaa1c43abda2e60bd1c95b3e24c9e9 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 6 Aug 2020 12:08:57 -0600 Subject: [PATCH 16/31] [maps] fix swap hidden/show icons in layer action panel (#74549) --- .../toc_entry_actions_popover.test.tsx.snap | 134 +++++++++++++++++- .../toc_entry_actions_popover.test.tsx | 25 +++- .../toc_entry_actions_popover.tsx | 2 +- 3 files changed, 151 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index 388712e1ebccad..8a5b7cf9186a8a 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -82,7 +82,7 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` "data-test-subj": "layerVisibilityToggleButton", "icon": , "name": "Hide layer", "onClick": [Function], @@ -210,7 +210,7 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou "data-test-subj": "layerVisibilityToggleButton", "icon": , "name": "Hide layer", "onClick": [Function], @@ -256,7 +256,7 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou `; -exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = ` +exports[`TOCEntryActionsPopover should have "show layer" action when layer is not visible 1`] = ` , + "name": "Show layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "editLayerButton", + "disabled": false, + "icon": , + "name": "Edit layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": , + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": , + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> + +`; + +exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = ` + + simulated tooltip content at zoom: 0 +
+ + mockFootnoteIcon + + + simulated footnote at isUsingSearch: true +
+ + } + delay="regular" + position="top" + title="layer 1" + > + + + + mockIcon + + + layer 1 + + + + + mockFootnoteIcon + + + + + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + , + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": , "name": "Hide layer", "onClick": [Function], "toolTipContent": null, diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx index c7ed5ec74ac7a6..95f13574105b7c 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -6,7 +6,7 @@ /* eslint-disable max-classes-per-file */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { AbstractLayer, ILayer } from '../../../../../../classes/layers/layer'; import { AbstractSource, ISource } from '../../../../../../classes/sources/source'; import { AbstractStyle, IStyle } from '../../../../../../classes/styles/style'; @@ -76,7 +76,7 @@ describe('TOCEntryActionsPopover', () => { }); test('is rendered', async () => { - const component = shallowWithIntl(); + const component = shallow(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -87,9 +87,7 @@ describe('TOCEntryActionsPopover', () => { }); test('should not show edit actions in read only mode', async () => { - const component = shallowWithIntl( - - ); + const component = shallow(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -101,7 +99,22 @@ describe('TOCEntryActionsPopover', () => { test('should disable fit to data when supportsFitToBounds is false', async () => { supportsFitToBounds = false; - const component = shallowWithIntl(); + const component = shallow(); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should have "show layer" action when layer is not visible', async () => { + const layer = new LayerMock(); + layer.isVisible = () => { + return false; + }; + const component = shallow(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index 5baac0a474ffaa..a1b9026fc57da7 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -158,7 +158,7 @@ export class TOCEntryActionsPopover extends Component { : i18n.translate('xpack.maps.layerTocActions.showLayerTitle', { defaultMessage: 'Show layer', }), - icon: , + icon: , 'data-test-subj': 'layerVisibilityToggleButton', toolTipContent: null, onClick: () => { From ebe46c0580bf397dad6e8e8abc72aca1875f76cc Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Thu, 6 Aug 2020 21:30:24 +0200 Subject: [PATCH 17/31] Make test less brittle when registry is changed. (#74554) --- x-pack/test/ingest_manager_api_integration/apis/epm/list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 0b6a37d77387e6..bfe1954e46c9f9 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(8); + expect(listResponse.response.length).not.to.be(0); } else { warnAndSkipTest(this, log); } From e807ddd1c1f8edcb0708fe2f365c511557986435 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 6 Aug 2020 15:33:24 -0400 Subject: [PATCH 18/31] [Monitoring] Handle getClient call throwing an exception (#74550) * This call can throw an exception in dist builds with ssl disabled * Fix typo --- .../lib/cluster/get_clusters_from_request.js | 106 +++++++++--------- x-pack/plugins/monitoring/server/plugin.ts | 18 ++- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 18db738bba38e6..16d42d896ca116 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -119,65 +119,67 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { const alertsClient = req.getAlertsClient(); - for (const cluster of clusters) { - const verification = verifyMonitoringLicense(req.server); - if (!verification.enabled) { - // return metadata detailing that alerts is disabled because of the monitoring cluster license - cluster.alerts = { - alertsMeta: { - enabled: verification.enabled, - message: verification.message, // NOTE: this is only defined when the alert feature is disabled - }, - list: {}, - }; - continue; - } + if (alertsClient) { + for (const cluster of clusters) { + const verification = verifyMonitoringLicense(req.server); + if (!verification.enabled) { + // return metadata detailing that alerts is disabled because of the monitoring cluster license + cluster.alerts = { + alertsMeta: { + enabled: verification.enabled, + message: verification.message, // NOTE: this is only defined when the alert feature is disabled + }, + list: {}, + }; + continue; + } + + // check the license type of the production cluster for alerts feature support + const license = cluster.license || {}; + const prodLicenseInfo = checkLicenseForAlerts( + license.type, + license.status === 'active', + 'production' + ); + if (prodLicenseInfo.clusterAlerts.enabled) { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + continue; + } - // check the license type of the production cluster for alerts feature support - const license = cluster.license || {}; - const prodLicenseInfo = checkLicenseForAlerts( - license.type, - license.status === 'active', - 'production' - ); - if (prodLicenseInfo.clusterAlerts.enabled) { cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid, - start, - end, - [] - ), + list: {}, alertsMeta: { enabled: true, }, + clusterMeta: { + enabled: false, + message: i18n.translate( + 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', + { + defaultMessage: + 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', + values: { + clusterName: cluster.cluster_name, + licenseType: `${license.type}`, + }, + } + ), + }, }; - continue; } - - cluster.alerts = { - list: {}, - alertsMeta: { - enabled: true, - }, - clusterMeta: { - enabled: false, - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', - { - defaultMessage: - 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', - values: { - clusterName: cluster.cluster_name, - licenseType: `${license.type}`, - }, - } - ), - }, - }; } } } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index ed091d4b8d7a7f..3aedb6831e7abd 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -325,8 +325,22 @@ export class Plugin { getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, getActionTypeRegistry: () => context.actions?.listTypes(), - getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req), - getActionsClient: () => plugins.actions.getActionsClientWithRequest(req), + getAlertsClient: () => { + try { + return plugins.alerts.getAlertsClientWithRequest(req); + } catch (err) { + // If security is disabled, this call will throw an error unless a certain config is set for dist builds + return null; + } + }, + getActionsClient: () => { + try { + return plugins.actions.getActionsClientWithRequest(req); + } catch (err) { + // If security is disabled, this call will throw an error unless a certain config is set for dist builds + return null; + } + }, server: { config: legacyConfigWrapper, newPlatform: { From 47fdd59e1c168a820260e2bd28a71843e4a0f3c1 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 6 Aug 2020 23:00:40 +0300 Subject: [PATCH 19/31] bump babel deps to support TS v4 (#74495) --- package.json | 10 +- packages/elastic-datemath/package.json | 4 +- packages/kbn-analytics/package.json | 2 +- packages/kbn-babel-preset/package.json | 16 +- packages/kbn-i18n/package.json | 4 +- packages/kbn-interpreter/package.json | 10 +- packages/kbn-optimizer/package.json | 2 +- packages/kbn-plugin-helpers/package.json | 2 +- packages/kbn-pm/package.json | 10 +- packages/kbn-test/package.json | 2 +- packages/kbn-ui-framework/package.json | 2 +- x-pack/package.json | 6 +- yarn.lock | 1289 +++++++++++----------- 13 files changed, 696 insertions(+), 663 deletions(-) diff --git a/package.json b/package.json index fa34fc3d0936a9..fc3af14ecae099 100644 --- a/package.json +++ b/package.json @@ -117,9 +117,9 @@ ] }, "dependencies": { - "@babel/core": "^7.10.2", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/register": "^7.10.1", + "@babel/core": "^7.11.1", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/register": "^7.10.5", "@elastic/apm-rum": "^5.2.0", "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", @@ -289,8 +289,8 @@ "yauzl": "2.10.0" }, "devDependencies": { - "@babel/parser": "^7.10.2", - "@babel/types": "^7.10.2", + "@babel/parser": "^7.11.2", + "@babel/types": "^7.11.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", diff --git a/packages/elastic-datemath/package.json b/packages/elastic-datemath/package.json index 15040a6243ff22..ad4190f9814397 100644 --- a/packages/elastic-datemath/package.json +++ b/packages/elastic-datemath/package.json @@ -11,8 +11,8 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "@babel/cli": "^7.10.1", - "@babel/preset-env": "^7.10.2", + "@babel/cli": "^7.10.5", + "@babel/preset-env": "^7.11.0", "babel-plugin-add-module-exports": "^1.0.2", "moment": "^2.24.0" }, diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index bd3f5832b71408..873252ceb0a1a9 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -14,7 +14,7 @@ "kbn:watch": "node scripts/build --source-maps --watch" }, "devDependencies": { - "@babel/cli": "^7.10.1", + "@babel/cli": "^7.10.5", "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", "typescript": "3.9.5" diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index 83530beffd2b2b..db1f2161b6e387 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -4,14 +4,14 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-export-namespace-from": "^7.10.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", - "@babel/plugin-proposal-optional-chaining": "^7.10.1", - "@babel/plugin-proposal-private-methods": "^7.10.1", - "@babel/preset-env": "^7.10.2", - "@babel/preset-react": "^7.10.1", - "@babel/preset-typescript": "^7.10.1", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/preset-env": "^7.11.0", + "@babel/preset-react": "^7.10.4", + "@babel/preset-typescript": "^7.10.4", "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", "babel-plugin-styled-components": "^1.10.7", diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index c5da144688c3c9..0f830acb284a05 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -12,8 +12,8 @@ "kbn:watch": "node scripts/build --watch --source-maps" }, "devDependencies": { - "@babel/cli": "^7.10.1", - "@babel/core": "^7.10.2", + "@babel/cli": "^7.10.5", + "@babel/core": "^7.11.1", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@types/intl-relativeformat": "^2.1.0", diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index c6bb06e68b9c04..aef63229ebe962 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -9,16 +9,16 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@babel/runtime": "^7.10.2", + "@babel/runtime": "^7.11.2", "@kbn/i18n": "1.0.0", "lodash": "^4.17.15", "uuid": "3.3.2" }, "devDependencies": { - "@babel/cli": "^7.10.1", - "@babel/core": "^7.10.2", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/plugin-transform-runtime": "^7.10.1", + "@babel/cli": "^7.10.5", + "@babel/core": "^7.11.1", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.11.0", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "babel-loader": "^8.0.6", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index e6eb5de31abd87..84e5c79e2e3589 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -10,7 +10,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@babel/cli": "^7.10.1", + "@babel/cli": "^7.10.5", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index f370265876df3d..45582ad2af97a0 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -12,7 +12,7 @@ "plugin-helpers": "bin/plugin-helpers.js" }, "dependencies": { - "@babel/core": "^7.10.2", + "@babel/core": "^7.11.1", "argv-split": "^2.0.1", "commander": "^3.0.0", "del": "^5.1.0", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 188db0a8321a26..3e40bf40222e65 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -10,11 +10,11 @@ "prettier": "prettier --write './src/**/*.ts'" }, "devDependencies": { - "@babel/core": "^7.10.2", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-object-rest-spread": "^7.10.1", - "@babel/preset-env": "^7.10.2", - "@babel/preset-typescript": "^7.10.1", + "@babel/core": "^7.11.1", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/preset-env": "^7.11.0", + "@babel/preset-typescript": "^7.10.4", "@types/cmd-shim": "^2.0.0", "@types/cpy": "^5.1.0", "@types/dedent": "^0.7.0", diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 38e4668fc1e428..9482ea83cc257b 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -10,7 +10,7 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "@babel/cli": "^7.10.1", + "@babel/cli": "^7.10.5", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@types/joi": "^13.4.2", diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 7933ce06d6847a..a095d9ac2a77f1 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -30,7 +30,7 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@babel/core": "^7.10.2", + "@babel/core": "^7.11.1", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", diff --git a/x-pack/package.json b/x-pack/package.json index dcba01a771fd5a..8fbb94c97c1435 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -199,9 +199,9 @@ "yargs": "4.8.1" }, "dependencies": { - "@babel/core": "^7.10.2", - "@babel/register": "^7.10.1", - "@babel/runtime": "^7.10.2", + "@babel/core": "^7.11.1", + "@babel/register": "^7.10.5", + "@babel/runtime": "^7.11.2", "@elastic/apm-rum-react": "^1.1.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", diff --git a/yarn.lock b/yarn.lock index f17418f07c5cc3..33083667a3c5eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,16 @@ # yarn lockfile v1 -"@babel/cli@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.1.tgz#b6e5cd43a17b8f639442ab027976408ebe6d79a0" - integrity sha512-cVB+dXeGhMOqViIaZs3A9OUAe4pKw4SBNdMw6yHJMYR7s4TB+Cei7ThquV/84O19PdIFWuwe03vxxES0BHUm5g== +"@babel/cli@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.5.tgz#57df2987c8cf89d0fc7d4b157ec59d7619f1b77a" + integrity sha512-j9H9qSf3kLdM0Ao3aGPbGZ73mEA9XazuupcS6cDGWuiyAcANoguhP0r2Lx32H5JGw4sSSoHG3x/mxVnHgvOoyA== dependencies: commander "^4.0.1" convert-source-map "^1.1.0" fs-readdir-recursive "^1.1.0" glob "^7.0.0" - lodash "^4.17.13" + lodash "^4.17.19" make-dir "^2.1.0" slash "^2.0.0" source-map "^0.5.0" @@ -32,17 +32,17 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/code-frame@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" - integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: - "@babel/highlight" "^7.10.1" + "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.1.tgz#b1085ffe72cd17bf2c0ee790fc09f9626011b2db" - integrity sha512-CHvCj7So7iCkGKPRFUfryXIkU2gSBw7VSZFYLsqVhrS47269VK2Hfi9S/YcublPMW8k1u2bQBlbDruoQEm4fgw== +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" + integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== dependencies: browserslist "^4.12.0" invariant "^2.2.4" @@ -79,24 +79,24 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a" - integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ== - dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/generator" "^7.10.2" - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helpers" "^7.10.1" - "@babel/parser" "^7.10.2" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.2" +"@babel/core@^7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" - lodash "^4.17.13" + lodash "^4.17.19" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" @@ -111,14 +111,13 @@ lodash "^4.17.13" source-map "^0.5.0" -"@babel/generator@^7.10.1", "@babel/generator@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9" - integrity sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA== +"@babel/generator@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== dependencies: - "@babel/types" "^7.10.2" + "@babel/types" "^7.11.0" jsesc "^2.5.1" - lodash "^4.17.13" source-map "^0.5.0" "@babel/generator@^7.9.5": @@ -138,20 +137,20 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-annotate-as-pure@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268" - integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw== +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.1.tgz#0ec7d9be8174934532661f87783eb18d72290059" - integrity sha512-cQpVq48EkYxUU0xozpGCLla3wlkdRRqLWu1ksFMXA9CM5KQmyyRpSEsYXbao7JUkOw/tAaYKCaYyZq6HOFYtyw== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" + integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== dependencies: - "@babel/helper-explode-assignable-expression" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-explode-assignable-expression" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": version "7.8.3" @@ -161,14 +160,14 @@ "@babel/helper-explode-assignable-expression" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-builder-react-jsx-experimental@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.1.tgz#9a7d58ad184d3ac3bafb1a452cec2bad7e4a0bc8" - integrity sha512-irQJ8kpQUV3JasXPSFQ+LCCtJSc5ceZrPFVj6TElR6XCHssi3jV8ch3odIrNtjJFRZZVbrOEfJMI79TPU/h1pQ== +"@babel/helper-builder-react-jsx-experimental@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" + integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-module-imports" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/types" "^7.10.5" "@babel/helper-builder-react-jsx-experimental@^7.9.0": version "7.9.0" @@ -179,13 +178,13 @@ "@babel/helper-module-imports" "^7.8.3" "@babel/types" "^7.9.0" -"@babel/helper-builder-react-jsx@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.1.tgz#a327f0cf983af5554701b1215de54a019f09b532" - integrity sha512-KXzzpyWhXgzjXIlJU1ZjIXzUPdej1suE6vzqgImZ/cpAsR/CC8gUcX4EWRmDfWz/cs6HOCPMBIJ3nKoXt3BFuw== +"@babel/helper-builder-react-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" + integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-builder-react-jsx@^7.9.0": version "7.9.0" @@ -195,12 +194,12 @@ "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/types" "^7.9.0" -"@babel/helper-compilation-targets@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz#a17d9723b6e2c750299d2a14d4637c76936d8285" - integrity sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA== +"@babel/helper-compilation-targets@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" + integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== dependencies: - "@babel/compat-data" "^7.10.1" + "@babel/compat-data" "^7.10.4" browserslist "^4.12.0" invariant "^2.2.4" levenary "^1.1.1" @@ -217,17 +216,17 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.1": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz#7474295770f217dbcf288bf7572eb213db46ee67" - integrity sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ== +"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-member-expression-to-functions" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-class-features-plugin@^7.8.3": version "7.8.6" @@ -241,13 +240,13 @@ "@babel/helper-replace-supers" "^7.8.6" "@babel/helper-split-export-declaration" "^7.8.3" -"@babel/helper-create-regexp-features-plugin@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd" - integrity sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA== +"@babel/helper-create-regexp-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" + integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-regex" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" "@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": @@ -259,14 +258,14 @@ "@babel/helper-regex" "^7.8.3" regexpu-core "^4.7.0" -"@babel/helper-define-map@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.1.tgz#5e69ee8308648470dd7900d159c044c10285221d" - integrity sha512-+5odWpX+OnvkD0Zmq7panrMuAGQBu6aPUgvMzuMGo4R+jUOvealEj2hiqI6WhxgKrTpFoFj0+VdsuA8KDxHBDg== +"@babel/helper-define-map@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/types" "^7.10.1" - lodash "^4.17.13" + "@babel/helper-function-name" "^7.10.4" + "@babel/types" "^7.10.5" + lodash "^4.17.19" "@babel/helper-define-map@^7.8.3": version "7.8.3" @@ -277,13 +276,13 @@ "@babel/types" "^7.8.3" lodash "^4.17.13" -"@babel/helper-explode-assignable-expression@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.1.tgz#e9d76305ee1162ca467357ae25df94f179af2b7e" - integrity sha512-vcUJ3cDjLjvkKzt6rHrl767FeE7pMEYfPanq5L16GRtrXIoznc0HykNW2aEYkcnP76P0isoqJ34dDMFZwzEpJg== +"@babel/helper-explode-assignable-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" + integrity sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A== dependencies: - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-explode-assignable-expression@^7.8.3": version "7.8.3" @@ -293,14 +292,14 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-function-name@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz#92bd63829bfc9215aca9d9defa85f56b539454f4" - integrity sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ== +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-function-name@^7.8.3": version "7.8.3" @@ -320,12 +319,12 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.9.5" -"@babel/helper-get-function-arity@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz#7303390a81ba7cb59613895a192b93850e373f7d" - integrity sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw== +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" @@ -334,12 +333,12 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-hoist-variables@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.1.tgz#7e77c82e5dcae1ebf123174c385aaadbf787d077" - integrity sha512-vLm5srkU8rI6X3+aQ1rQJyfjvCBLXP8cAGeuw04zeAM2ItKb1e7pmVmLyHb4sDaAYnLL13RHOZPLEtcGZ5xvjg== +"@babel/helper-hoist-variables@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" + integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" "@babel/helper-hoist-variables@^7.8.3": version "7.8.3" @@ -348,12 +347,12 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-member-expression-to-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz#432967fd7e12a4afef66c4687d4ca22bc0456f15" - integrity sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g== +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.11.0" "@babel/helper-member-expression-to-functions@^7.8.3": version "7.8.3" @@ -369,25 +368,25 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-module-imports@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz#dd331bd45bccc566ce77004e9d05fe17add13876" - integrity sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg== +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622" - integrity sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-simple-access" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" - lodash "^4.17.13" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" "@babel/helper-module-transforms@^7.9.0": version "7.9.0" @@ -402,12 +401,12 @@ "@babel/types" "^7.9.0" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz#b4a1f2561870ce1247ceddb02a3860fa96d72543" - integrity sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg== +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" "@babel/helper-optimise-call-expression@^7.8.3": version "7.8.3" @@ -421,17 +420,17 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== -"@babel/helper-plugin-utils@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127" - integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA== +"@babel/helper-plugin-utils@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96" - integrity sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g== +"@babel/helper-regex@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== dependencies: - lodash "^4.17.13" + lodash "^4.17.19" "@babel/helper-regex@^7.8.3": version "7.8.3" @@ -440,16 +439,16 @@ dependencies: lodash "^4.17.13" -"@babel/helper-remap-async-to-generator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.1.tgz#bad6aaa4ff39ce8d4b82ccaae0bfe0f7dbb5f432" - integrity sha512-RfX1P8HqsfgmJ6CwaXGKMAqbYdlleqglvVtht0HGPMSsy2V6MqLlOJVF/0Qyb/m2ZCi2z3q3+s6Pv7R/dQuZ6A== +"@babel/helper-remap-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" + integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-wrap-function" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-remap-async-to-generator@^7.8.3": version "7.8.3" @@ -462,15 +461,15 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-replace-supers@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" - integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A== +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": version "7.8.6" @@ -482,13 +481,13 @@ "@babel/traverse" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/helper-simple-access@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" - integrity sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw== +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== dependencies: - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-simple-access@^7.8.3": version "7.8.3" @@ -498,12 +497,19 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-split-export-declaration@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" - integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g== +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.11.0" "@babel/helper-split-export-declaration@^7.8.3": version "7.8.3" @@ -512,10 +518,10 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-validator-identifier@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" - integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== "@babel/helper-validator-identifier@^7.9.0": version "7.9.0" @@ -527,15 +533,15 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== -"@babel/helper-wrap-function@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9" - integrity sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ== +"@babel/helper-wrap-function@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" + integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helper-wrap-function@^7.8.3": version "7.8.3" @@ -547,14 +553,14 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" - integrity sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw== +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== dependencies: - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/helpers@^7.9.0": version "7.9.2" @@ -574,12 +580,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/highlight@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" - integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== dependencies: - "@babel/helper-validator-identifier" "^7.10.1" + "@babel/helper-validator-identifier" "^7.10.4" chalk "^2.0.0" js-tokens "^4.0.0" @@ -588,18 +594,18 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== -"@babel/parser@^7.10.1", "@babel/parser@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" - integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== +"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" + integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== -"@babel/plugin-proposal-async-generator-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.1.tgz#6911af5ba2e615c4ff3c497fe2f47b35bf6d7e55" - integrity sha512-vzZE12ZTdB336POZjmpblWfNNRpMSua45EYnRigE2XsZxcXcIyly2ixnTJasJE4Zq3U7t2d8rRF7XRUuzHxbOw== +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-remap-async-to-generator" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" "@babel/plugin-proposal-async-generator-functions@^7.8.3": @@ -611,13 +617,13 @@ "@babel/helper-remap-async-to-generator" "^7.8.3" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01" - integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw== +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-proposal-class-properties@^7.7.0": version "7.8.3" @@ -627,12 +633,12 @@ "@babel/helper-create-class-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-proposal-dynamic-import@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0" - integrity sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA== +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" + integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-proposal-dynamic-import@^7.8.3": @@ -643,20 +649,20 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-namespace-from@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.1.tgz#512ee069cd866256600bdf89639cf7e1b51fbfe9" - integrity sha512-eR4CoYb6mh5y9LWjnb4CyUatuhtZ8pNLXLDi46GkqtF7WPafFqXycHdvF5qWviozZVGRSAmHzdayc8wUReCdjA== +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09" - integrity sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg== +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" + integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-proposal-json-strings@^7.8.3": @@ -667,12 +673,20 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78" - integrity sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA== +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" "@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": @@ -683,13 +697,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123" - integrity sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA== +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" + integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-numeric-separator" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-numeric-separator@^7.8.3": version "7.8.3" @@ -699,14 +713,14 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-numeric-separator" "^7.8.3" -"@babel/plugin-proposal-object-rest-spread@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.1.tgz#cba44908ac9f142650b4a65b8aa06bf3478d5fb6" - integrity sha512-Z+Qri55KiQkHh7Fc4BW6o+QBuTagbOp9txE+4U1i79u9oWlf2npkiDx+Rf3iK3lbcHBuNy9UOkwuR5wOMH3LIQ== +"@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.1" + "@babel/plugin-transform-parameters" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.9.0": version "7.9.0" @@ -716,12 +730,12 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" -"@babel/plugin-proposal-optional-catch-binding@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz#c9f86d99305f9fa531b568ff5ab8c964b8b223d2" - integrity sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA== +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" + integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-proposal-optional-catch-binding@^7.8.3": @@ -732,12 +746,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.1.tgz#15f5d6d22708629451a91be28f8facc55b0e818c" - integrity sha512-dqQj475q8+/avvok72CF3AOSV/SGEcH29zT5hhohqqvvZ2+boQoOr7iGldBG5YXTO2qgCgc2B3WvVLUdbeMlGA== +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" "@babel/plugin-proposal-optional-chaining@^7.9.0": @@ -748,21 +763,21 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-private-methods@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598" - integrity sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg== +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f" - integrity sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ== +"@babel/plugin-proposal-unicode-property-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" + integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": version "7.8.8" @@ -786,12 +801,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5" - integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ== +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-class-properties@^7.8.3": version "7.8.3" @@ -828,12 +843,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz#0ae371134a42b91d5418feb3c8c8d43e1565d2da" - integrity sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ== +"@babel/plugin-syntax-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c" + integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx@^7.8.3": version "7.8.3" @@ -842,6 +857,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" @@ -856,12 +878,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99" - integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg== +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.8.3" @@ -891,12 +913,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz#8b8733f8c57397b3eaa47ddba8841586dcaef362" - integrity sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ== +"@babel/plugin-syntax-top-level-await@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" + integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.8.3" @@ -905,19 +927,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-typescript@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz#5e82bc27bb4202b93b949b029e699db536733810" - integrity sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q== +"@babel/plugin-syntax-typescript@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" + integrity sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b" - integrity sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA== +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" + integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.8.3" @@ -926,14 +948,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-async-to-generator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz#e5153eb1a3e028f79194ed8a7a4bf55f862b2062" - integrity sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg== +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" + integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-remap-async-to-generator" "^7.10.1" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-transform-async-to-generator@^7.8.3": version "7.8.3" @@ -944,12 +966,12 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-remap-async-to-generator" "^7.8.3" -"@babel/plugin-transform-block-scoped-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz#146856e756d54b20fff14b819456b3e01820b85d" - integrity sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q== +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" + integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-block-scoped-functions@^7.8.3": version "7.8.3" @@ -958,13 +980,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-block-scoping@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz#47092d89ca345811451cd0dc5d91605982705d5e" - integrity sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw== +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" + integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - lodash "^4.17.13" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-block-scoping@^7.8.3": version "7.8.3" @@ -974,18 +995,18 @@ "@babel/helper-plugin-utils" "^7.8.3" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.1.tgz#6e11dd6c4dfae70f540480a4702477ed766d733f" - integrity sha512-P9V0YIh+ln/B3RStPoXpEQ/CoAxQIhRSUn7aXqQ+FZJ2u8+oCtjIXR3+X0vsSD8zv+mb56K7wZW1XiDTDGiDRQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-define-map" "^7.10.1" - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" +"@babel/plugin-transform-classes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" + integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" "@babel/plugin-transform-classes@^7.9.0": @@ -1002,12 +1023,12 @@ "@babel/helper-split-export-declaration" "^7.8.3" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.1.tgz#59aa399064429d64dce5cf76ef9b90b7245ebd07" - integrity sha512-mqSrGjp3IefMsXIenBfGcPXxJxweQe2hEIwMQvjtiDQ9b1IBvDUjkAtV/HMXX47/vXf14qDNedXsIiNd1FmkaQ== +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" + integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-computed-properties@^7.8.3": version "7.8.3" @@ -1016,12 +1037,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-destructuring@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz#abd58e51337815ca3a22a336b85f62b998e71907" - integrity sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA== +"@babel/plugin-transform-destructuring@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" + integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-destructuring@^7.8.3": version "7.8.8" @@ -1030,13 +1051,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-dotall-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz#920b9fec2d78bb57ebb64a644d5c2ba67cc104ee" - integrity sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA== +"@babel/plugin-transform-dotall-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" + integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": version "7.8.3" @@ -1046,12 +1067,12 @@ "@babel/helper-create-regexp-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-duplicate-keys@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz#c900a793beb096bc9d4d0a9d0cde19518ffc83b9" - integrity sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA== +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" + integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-duplicate-keys@^7.8.3": version "7.8.3" @@ -1060,13 +1081,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz#279c3116756a60dd6e6f5e488ba7957db9c59eb3" - integrity sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA== +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" + integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-exponentiation-operator@^7.8.3": version "7.8.3" @@ -1084,12 +1105,12 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-flow" "^7.8.3" -"@babel/plugin-transform-for-of@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz#ff01119784eb0ee32258e8646157ba2501fcfda5" - integrity sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w== +"@babel/plugin-transform-for-of@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" + integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-for-of@^7.9.0": version "7.9.0" @@ -1098,13 +1119,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-function-name@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz#4ed46fd6e1d8fde2a2ec7b03c66d853d2c92427d" - integrity sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw== +"@babel/plugin-transform-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" + integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-function-name@^7.8.3": version "7.8.3" @@ -1114,12 +1135,12 @@ "@babel/helper-function-name" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz#5794f8da82846b22e4e6631ea1658bce708eb46a" - integrity sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw== +"@babel/plugin-transform-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" + integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-literals@^7.8.3": version "7.8.3" @@ -1128,12 +1149,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-member-expression-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz#90347cba31bca6f394b3f7bd95d2bbfd9fce2f39" - integrity sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA== +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" + integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-member-expression-literals@^7.8.3": version "7.8.3" @@ -1142,13 +1163,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz#65950e8e05797ebd2fe532b96e19fc5482a1d52a" - integrity sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw== +"@babel/plugin-transform-modules-amd@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-amd@^7.9.0": @@ -1160,14 +1181,14 @@ "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301" - integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg== +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-simple-access" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.9.0": @@ -1180,14 +1201,14 @@ "@babel/helper-simple-access" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-systemjs@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.1.tgz#9962e4b0ac6aaf2e20431ada3d8ec72082cbffb6" - integrity sha512-ewNKcj1TQZDL3YnO85qh9zo1YF1CHgmSTlRQgHqe63oTrMI85cthKtZjAiZSsSNjPQ5NCaYo5QkbYqEw1ZBgZA== +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== dependencies: - "@babel/helper-hoist-variables" "^7.10.1" - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.9.0": @@ -1200,13 +1221,13 @@ "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-umd@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz#ea080911ffc6eb21840a5197a39ede4ee67b1595" - integrity sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA== +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" + integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-modules-umd@^7.9.0": version "7.9.0" @@ -1216,6 +1237,13 @@ "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" + integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" @@ -1223,12 +1251,12 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.8.3" -"@babel/plugin-transform-new-target@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz#6ee41a5e648da7632e22b6fb54012e87f612f324" - integrity sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw== +"@babel/plugin-transform-new-target@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" + integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-new-target@^7.8.3": version "7.8.3" @@ -1237,13 +1265,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-object-super@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz#2e3016b0adbf262983bf0d5121d676a5ed9c4fde" - integrity sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw== +"@babel/plugin-transform-object-super@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" + integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" "@babel/plugin-transform-object-super@^7.8.3": version "7.8.3" @@ -1253,13 +1281,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-replace-supers" "^7.8.3" -"@babel/plugin-transform-parameters@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd" - integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg== +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-parameters@^7.8.7": version "7.9.3" @@ -1269,12 +1297,12 @@ "@babel/helper-get-function-arity" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-property-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz#cffc7315219230ed81dc53e4625bf86815b6050d" - integrity sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA== +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" + integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-property-literals@^7.8.3": version "7.8.3" @@ -1290,12 +1318,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-react-display-name@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.1.tgz#e6a33f6d48dfb213dda5e007d0c7ff82b6a3d8ef" - integrity sha512-rBjKcVwjk26H3VX8pavMxGf33LNlbocMHdSeldIEswtQ/hrjyTG8fKKILW1cSkODyRovckN/uZlGb2+sAV9JUQ== +"@babel/plugin-transform-react-display-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz#b5795f4e3e3140419c3611b7a2a3832b9aef328d" + integrity sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-react-display-name@^7.8.3": version "7.8.3" @@ -1304,14 +1332,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-react-jsx-development@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.1.tgz#1ac6300d8b28ef381ee48e6fec430cc38047b7f3" - integrity sha512-XwDy/FFoCfw9wGFtdn5Z+dHh6HXKHkC6DwKNWpN74VWinUagZfDcEJc3Y8Dn5B3WMVnAllX8Kviaw7MtC5Epwg== +"@babel/plugin-transform-react-jsx-development@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" + integrity sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ== dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-builder-react-jsx-experimental" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" "@babel/plugin-transform-react-jsx-development@^7.9.0": version "7.9.0" @@ -1322,13 +1350,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-jsx-self@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz#22143e14388d72eb88649606bb9e46f421bc3821" - integrity sha512-4p+RBw9d1qV4S749J42ZooeQaBomFPrSxa9JONLHJ1TxCBo3TzJ79vtmG2S2erUT8PDDrPdw4ZbXGr2/1+dILA== +"@babel/plugin-transform-react-jsx-self@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" + integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" "@babel/plugin-transform-react-jsx-self@^7.9.0": version "7.9.0" @@ -1338,13 +1366,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-jsx-source@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.1.tgz#30db3d4ee3cdebbb26a82a9703673714777a4273" - integrity sha512-neAbaKkoiL+LXYbGDvh6PjPG+YeA67OsZlE78u50xbWh2L1/C81uHiNP5d1fw+uqUIoiNdCC8ZB+G4Zh3hShJA== +"@babel/plugin-transform-react-jsx-source@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" + integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" "@babel/plugin-transform-react-jsx-source@^7.9.0": version "7.9.0" @@ -1354,15 +1382,15 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-jsx@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.1.tgz#91f544248ba131486decb5d9806da6a6e19a2896" - integrity sha512-MBVworWiSRBap3Vs39eHt+6pJuLUAaK4oxGc8g+wY+vuSJvLiEQjW1LSTqKb8OUPtDvHCkdPhk7d6sjC19xyFw== +"@babel/plugin-transform-react-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2" + integrity sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A== dependencies: - "@babel/helper-builder-react-jsx" "^7.10.1" - "@babel/helper-builder-react-jsx-experimental" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-builder-react-jsx" "^7.10.4" + "@babel/helper-builder-react-jsx-experimental" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" "@babel/plugin-transform-react-jsx@^7.9.4": version "7.9.4" @@ -1374,18 +1402,18 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-pure-annotations@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.1.tgz#f5e7c755d3e7614d4c926e144f501648a5277b70" - integrity sha512-mfhoiai083AkeewsBHUpaS/FM1dmUENHBMpS/tugSJ7VXqXO5dCN1Gkint2YvM1Cdv1uhmAKt1ZOuAjceKmlLA== +"@babel/plugin-transform-react-pure-annotations@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" + integrity sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-regenerator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.1.tgz#10e175cbe7bdb63cc9b39f9b3f823c5c7c5c5490" - integrity sha512-B3+Y2prScgJ2Bh/2l9LJxKbb8C8kRfsG4AdPT+n7ixBHIxJaIG8bi8tgjxUMege1+WqSJ+7gu1YeoMVO3gPWzw== +"@babel/plugin-transform-regenerator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" + integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== dependencies: regenerator-transform "^0.14.2" @@ -1396,12 +1424,12 @@ dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz#0fc1027312b4d1c3276a57890c8ae3bcc0b64a86" - integrity sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ== +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" + integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-reserved-words@^7.8.3": version "7.8.3" @@ -1410,22 +1438,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-runtime@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1" - integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw== +"@babel/plugin-transform-runtime@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" + integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3" - integrity sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g== +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" + integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-shorthand-properties@^7.8.3": version "7.8.3" @@ -1434,12 +1462,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-spread@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz#0c6d618a0c4461a274418460a28c9ccf5239a7c8" - integrity sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw== +"@babel/plugin-transform-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" + integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-transform-spread@^7.8.3": version "7.8.3" @@ -1448,13 +1477,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-sticky-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz#90fc89b7526228bed9842cff3588270a7a393b00" - integrity sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA== +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" + integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-regex" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-regex" "^7.10.4" "@babel/plugin-transform-sticky-regex@^7.8.3": version "7.8.3" @@ -1464,13 +1493,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-regex" "^7.8.3" -"@babel/plugin-transform-template-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.1.tgz#914c7b7f4752c570ea00553b4284dad8070e8628" - integrity sha512-t7B/3MQf5M1T9hPCRG28DNGZUuxAuDqLYS03rJrIk2prj/UV7Z6FOneijhQhnv/Xa039vidXeVbvjK2SK5f7Gg== +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-template-literals@^7.8.3": version "7.8.3" @@ -1480,12 +1509,12 @@ "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-typeof-symbol@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz#60c0239b69965d166b80a84de7315c1bc7e0bb0e" - integrity sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g== +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" + integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-typeof-symbol@^7.8.4": version "7.8.4" @@ -1494,29 +1523,29 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-typescript@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz#2c54daea231f602468686d9faa76f182a94507a6" - integrity sha512-v+QWKlmCnsaimLeqq9vyCsVRMViZG1k2SZTlcZvB+TqyH570Zsij8nvVUZzOASCRiQFUxkLrn9Wg/kH0zgy5OQ== +"@babel/plugin-transform-typescript@^7.10.4": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz#2b4879676af37342ebb278216dd090ac67f13abb" + integrity sha512-edJsNzTtvb3MaXQwj8403B7mZoGu9ElDJQZOKjGUnvilquxBA3IQoEIOvkX/1O8xfAsnHS/oQhe2w/IXrr+w0w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-typescript" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.10.4" -"@babel/plugin-transform-unicode-escapes@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940" - integrity sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw== +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f" - integrity sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw== +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" + integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-unicode-regex@^7.8.3": version "7.8.3" @@ -1592,70 +1621,74 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/preset-env@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.2.tgz#715930f2cf8573b0928005ee562bed52fb65fdfb" - integrity sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA== - dependencies: - "@babel/compat-data" "^7.10.1" - "@babel/helper-compilation-targets" "^7.10.2" - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-proposal-async-generator-functions" "^7.10.1" - "@babel/plugin-proposal-class-properties" "^7.10.1" - "@babel/plugin-proposal-dynamic-import" "^7.10.1" - "@babel/plugin-proposal-json-strings" "^7.10.1" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.1" - "@babel/plugin-proposal-numeric-separator" "^7.10.1" - "@babel/plugin-proposal-object-rest-spread" "^7.10.1" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.1" - "@babel/plugin-proposal-optional-chaining" "^7.10.1" - "@babel/plugin-proposal-private-methods" "^7.10.1" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.1" +"@babel/preset-env@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" + integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + dependencies: + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.1" + "@babel/plugin-syntax-class-properties" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.10.1" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.1" - "@babel/plugin-transform-arrow-functions" "^7.10.1" - "@babel/plugin-transform-async-to-generator" "^7.10.1" - "@babel/plugin-transform-block-scoped-functions" "^7.10.1" - "@babel/plugin-transform-block-scoping" "^7.10.1" - "@babel/plugin-transform-classes" "^7.10.1" - "@babel/plugin-transform-computed-properties" "^7.10.1" - "@babel/plugin-transform-destructuring" "^7.10.1" - "@babel/plugin-transform-dotall-regex" "^7.10.1" - "@babel/plugin-transform-duplicate-keys" "^7.10.1" - "@babel/plugin-transform-exponentiation-operator" "^7.10.1" - "@babel/plugin-transform-for-of" "^7.10.1" - "@babel/plugin-transform-function-name" "^7.10.1" - "@babel/plugin-transform-literals" "^7.10.1" - "@babel/plugin-transform-member-expression-literals" "^7.10.1" - "@babel/plugin-transform-modules-amd" "^7.10.1" - "@babel/plugin-transform-modules-commonjs" "^7.10.1" - "@babel/plugin-transform-modules-systemjs" "^7.10.1" - "@babel/plugin-transform-modules-umd" "^7.10.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.10.1" - "@babel/plugin-transform-object-super" "^7.10.1" - "@babel/plugin-transform-parameters" "^7.10.1" - "@babel/plugin-transform-property-literals" "^7.10.1" - "@babel/plugin-transform-regenerator" "^7.10.1" - "@babel/plugin-transform-reserved-words" "^7.10.1" - "@babel/plugin-transform-shorthand-properties" "^7.10.1" - "@babel/plugin-transform-spread" "^7.10.1" - "@babel/plugin-transform-sticky-regex" "^7.10.1" - "@babel/plugin-transform-template-literals" "^7.10.1" - "@babel/plugin-transform-typeof-symbol" "^7.10.1" - "@babel/plugin-transform-unicode-escapes" "^7.10.1" - "@babel/plugin-transform-unicode-regex" "^7.10.1" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.10.2" + "@babel/types" "^7.11.0" browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" @@ -1693,34 +1726,34 @@ "@babel/plugin-transform-react-jsx-self" "^7.9.0" "@babel/plugin-transform-react-jsx-source" "^7.9.0" -"@babel/preset-react@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.1.tgz#e2ab8ae9a363ec307b936589f07ed753192de041" - integrity sha512-Rw0SxQ7VKhObmFjD/cUcKhPTtzpeviEFX1E6PgP+cYOhQ98icNqtINNFANlsdbQHrmeWnqdxA4Tmnl1jy5tp3Q== +"@babel/preset-react@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" + integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-transform-react-display-name" "^7.10.1" - "@babel/plugin-transform-react-jsx" "^7.10.1" - "@babel/plugin-transform-react-jsx-development" "^7.10.1" - "@babel/plugin-transform-react-jsx-self" "^7.10.1" - "@babel/plugin-transform-react-jsx-source" "^7.10.1" - "@babel/plugin-transform-react-pure-annotations" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-display-name" "^7.10.4" + "@babel/plugin-transform-react-jsx" "^7.10.4" + "@babel/plugin-transform-react-jsx-development" "^7.10.4" + "@babel/plugin-transform-react-jsx-self" "^7.10.4" + "@babel/plugin-transform-react-jsx-source" "^7.10.4" + "@babel/plugin-transform-react-pure-annotations" "^7.10.4" -"@babel/preset-typescript@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.1.tgz#a8d8d9035f55b7d99a2461a0bdc506582914d07e" - integrity sha512-m6GV3y1ShiqxnyQj10600ZVOFrSSAa8HQ3qIUk2r+gcGtHTIRw0dJnFLt1WNXpKjtVw7yw1DAPU/6ma2ZvgJuA== +"@babel/preset-typescript@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.4.tgz#7d5d052e52a682480d6e2cc5aa31be61c8c25e36" + integrity sha512-SdYnvGPv+bLlwkF2VkJnaX/ni1sMNetcGI1+nThF1gyv6Ph8Qucc4ZZAjM5yZcE/AKRXIOTZz7eSRDWOEjPyRQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-transform-typescript" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-typescript" "^7.10.4" -"@babel/register@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.10.1.tgz#b6567c5cb5049f44bbf8c35d6ff68ca3c43238ed" - integrity sha512-sl96+kB3IA2B9EzpwwBmYadOT14vw3KaXOknGDbJaZCOj52GDA4Tivudq9doCJcB+bEIKCEARZYwRgBBsCGXyg== +"@babel/register@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.10.5.tgz#354f3574895f1307f79efe37a51525e52fd38d89" + integrity sha512-eYHdLv43nyvmPn9bfNfrcC4+iYNwdQ8Pxk1MFJuU/U5LpSYl/PH4dFMazCYZDFVi8ueG3shvO+AQfLrxpYulQw== dependencies: find-cache-dir "^2.0.0" - lodash "^4.17.13" + lodash "^4.17.19" make-dir "^2.1.0" pirates "^4.0.0" source-map-support "^0.5.16" @@ -1747,10 +1780,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" - integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== +"@babel/runtime@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: regenerator-runtime "^0.13.4" @@ -1775,14 +1808,14 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/template@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" - integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig== +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/parser" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": version "7.9.0" @@ -1799,20 +1832,20 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/traverse@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27" - integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ== - dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/generator" "^7.10.1" - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" - "@babel/parser" "^7.10.1" - "@babel/types" "^7.10.1" +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" + integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.0" + "@babel/types" "^7.11.0" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.13" + lodash "^4.17.19" "@babel/traverse@^7.7.4": version "7.9.5" @@ -1838,13 +1871,13 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.10.1", "@babel/types@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d" - integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" + integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== dependencies: - "@babel/helper-validator-identifier" "^7.10.1" - lodash "^4.17.13" + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" to-fast-properties "^2.0.0" "@babel/types@^7.3.3": @@ -20330,7 +20363,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.19, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.16, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.19, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.16, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== From 810ff87afbac92aa8b655dc944b27894f1c4682a Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 6 Aug 2020 16:29:09 -0400 Subject: [PATCH 20/31] [Security Solution][Resolver] standardize resolver panel component naming (#74537) --- ...ess_cube_icon.tsx => cube_for_process.tsx} | 90 +-- ...ounts.tsx => event_counts_for_process.tsx} | 288 +++---- .../view/{panel.tsx => panels/index.tsx} | 28 +- ...process_detail.tsx => process_details.tsx} | 2 +- ...elated_list.tsx => process_event_list.tsx} | 580 +++++++------- ..._list.tsx => process_list_with_counts.tsx} | 2 +- ...ed_detail.tsx => related_event_detail.tsx} | 750 +++++++++--------- .../public/resolver/view/styles.tsx | 2 +- 8 files changed, 871 insertions(+), 871 deletions(-) rename x-pack/plugins/security_solution/public/resolver/view/panels/{process_cube_icon.tsx => cube_for_process.tsx} (96%) rename x-pack/plugins/security_solution/public/resolver/view/panels/{panel_content_related_counts.tsx => event_counts_for_process.tsx} (97%) rename x-pack/plugins/security_solution/public/resolver/view/{panel.tsx => panels/index.tsx} (90%) rename x-pack/plugins/security_solution/public/resolver/view/panels/{panel_content_process_detail.tsx => process_details.tsx} (98%) rename x-pack/plugins/security_solution/public/resolver/view/panels/{panel_content_related_list.tsx => process_event_list.tsx} (95%) rename x-pack/plugins/security_solution/public/resolver/view/panels/{panel_content_process_list.tsx => process_list_with_counts.tsx} (99%) rename x-pack/plugins/security_solution/public/resolver/view/panels/{panel_content_related_detail.tsx => related_event_detail.tsx} (97%) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index b073324b27f9bd..0d8f65b4e39e62 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -1,45 +1,45 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { memo } from 'react'; -import { useResolverTheme } from '../assets'; - -/** - * During user testing, one user indicated they wanted to see stronger visual relationships between - * Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that. - */ -export const CubeForProcess = memo(function CubeForProcess({ - isProcessTerminated, -}: { - isProcessTerminated: boolean; -}) { - const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, false); - - return ( - <> - - {descriptionText} - - - - ); -}); +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { useResolverTheme } from '../assets'; + +/** + * During user testing, one user indicated they wanted to see stronger visual relationships between + * Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that. + */ +export const CubeForProcess = memo(function CubeForProcess({ + isProcessTerminated, +}: { + isProcessTerminated: boolean; +}) { + const { cubeAssetsForNode } = useResolverTheme(); + const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, false); + + return ( + <> + + {descriptionText} + + + + ); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx index 880ee1dc7a10a0..129aff776808ad 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx @@ -1,144 +1,144 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { memo, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; -import { FormattedMessage } from 'react-intl'; -import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities'; - -import * as event from '../../../../common/endpoint/models/event'; -import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; - -/** - * This view gives counts for all the related events of a process grouped by related event type. - * It should look something like: - * - * | Count | Event Type | - * | :--------------------- | :------------------------- | - * | 5 | DNS | - * | 12 | Registry | - * | 2 | Network | - * - */ -export const EventCountsForProcess = memo(function EventCountsForProcess({ - processEvent, - pushToQueryParams, - relatedStats, -}: { - processEvent: ResolverEvent; - pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; - relatedStats: ResolverNodeStats; -}) { - interface EventCountsTableView { - name: string; - count: number; - } - - const relatedEventsState = { stats: relatedStats.events.byCategory }; - const processName = processEvent && event.eventName(processEvent); - const processEntityId = event.entityId(processEvent); - /** - * totalCount: This will reflect the aggregated total by category for all related events - * e.g. [dns,file],[dns,file],[registry] will have an aggregate total of 5. This is to keep the - * total number consistent with the "broken out" totals we see elsewhere in the app. - * E.g. on the rleated list by type, the above would show as: - * 2 dns - * 2 file - * 1 registry - * So it would be extremely disorienting to show the user a "3" above that as a total. - */ - const totalCount = Object.values(relatedStats.events.byCategory).reduce( - (sum, val) => sum + val, - 0 - ); - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events', - { - defaultMessage: 'Events', - } - ); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - { - text: processName, - onClick: () => { - pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => { - pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); - }, - }, - ]; - }, [processName, totalCount, processEntityId, pushToQueryParams, eventsString]); - const rows = useMemo(() => { - return Object.entries(relatedEventsState.stats).map( - ([eventType, count]): EventCountsTableView => { - return { - name: eventType, - count, - }; - } - ); - }, [relatedEventsState]); - const columns = useMemo>>( - () => [ - { - field: 'count', - name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.count', { - defaultMessage: 'Count', - }), - width: '20%', - sortable: true, - }, - { - field: 'name', - name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.eventType', { - defaultMessage: 'Event Type', - }), - width: '80%', - sortable: true, - render(name: string) { - return ( - { - pushToQueryParams({ crumbId: event.entityId(processEvent), crumbEvent: name }); - }} - > - {name} - - ); - }, - }, - ], - [pushToQueryParams, processEvent] - ); - return ( - <> - - - items={rows} columns={columns} sorting /> - - ); -}); -EventCountsForProcess.displayName = 'EventCountsForProcess'; +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; +import { FormattedMessage } from 'react-intl'; +import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities'; + +import * as event from '../../../../common/endpoint/models/event'; +import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; + +/** + * This view gives counts for all the related events of a process grouped by related event type. + * It should look something like: + * + * | Count | Event Type | + * | :--------------------- | :------------------------- | + * | 5 | DNS | + * | 12 | Registry | + * | 2 | Network | + * + */ +export const EventCountsForProcess = memo(function EventCountsForProcess({ + processEvent, + pushToQueryParams, + relatedStats, +}: { + processEvent: ResolverEvent; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; + relatedStats: ResolverNodeStats; +}) { + interface EventCountsTableView { + name: string; + count: number; + } + + const relatedEventsState = { stats: relatedStats.events.byCategory }; + const processName = processEvent && event.eventName(processEvent); + const processEntityId = event.entityId(processEvent); + /** + * totalCount: This will reflect the aggregated total by category for all related events + * e.g. [dns,file],[dns,file],[registry] will have an aggregate total of 5. This is to keep the + * total number consistent with the "broken out" totals we see elsewhere in the app. + * E.g. on the rleated list by type, the above would show as: + * 2 dns + * 2 file + * 1 registry + * So it would be extremely disorienting to show the user a "3" above that as a total. + */ + const totalCount = Object.values(relatedStats.events.byCategory).reduce( + (sum, val) => sum + val, + 0 + ); + const eventsString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events', + { + defaultMessage: 'Events', + } + ); + const crumbs = useMemo(() => { + return [ + { + text: eventsString, + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + { + text: processName, + onClick: () => { + pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => { + pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); + }, + }, + ]; + }, [processName, totalCount, processEntityId, pushToQueryParams, eventsString]); + const rows = useMemo(() => { + return Object.entries(relatedEventsState.stats).map( + ([eventType, count]): EventCountsTableView => { + return { + name: eventType, + count, + }; + } + ); + }, [relatedEventsState]); + const columns = useMemo>>( + () => [ + { + field: 'count', + name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.count', { + defaultMessage: 'Count', + }), + width: '20%', + sortable: true, + }, + { + field: 'name', + name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.eventType', { + defaultMessage: 'Event Type', + }), + width: '80%', + sortable: true, + render(name: string) { + return ( + { + pushToQueryParams({ crumbId: event.entityId(processEvent), crumbEvent: name }); + }} + > + {name} + + ); + }, + }, + ], + [pushToQueryParams, processEvent] + ); + return ( + <> + + + items={rows} columns={columns} sorting /> + + ); +}); +EventCountsForProcess.displayName = 'EventCountsForProcess'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/resolver/view/panel.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index f378ab36bac945..7e7e8b757baf75 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -7,17 +7,17 @@ import React, { memo, useMemo, useContext, useLayoutEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { EuiPanel } from '@elastic/eui'; -import * as selectors from '../store/selectors'; -import { useResolverDispatch } from './use_resolver_dispatch'; -import * as event from '../../../common/endpoint/models/event'; -import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; -import { SideEffectContext } from './side_effect_context'; -import { ProcessEventListNarrowedByType } from './panels/panel_content_related_list'; -import { EventCountsForProcess } from './panels/panel_content_related_counts'; -import { ProcessDetails } from './panels/panel_content_process_detail'; -import { ProcessListWithCounts } from './panels/panel_content_process_list'; -import { RelatedEventDetail } from './panels/panel_content_related_detail'; -import { useResolverQueryParams } from './use_resolver_query_params'; +import * as selectors from '../../store/selectors'; +import { useResolverDispatch } from '../use_resolver_dispatch'; +import * as event from '../../../../common/endpoint/models/event'; +import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; +import { SideEffectContext } from '../side_effect_context'; +import { ProcessEventList } from './process_event_list'; +import { EventCountsForProcess } from './event_counts_for_process'; +import { ProcessDetails } from './process_details'; +import { ProcessListWithCounts } from './process_list_with_counts'; +import { RelatedEventDetail } from './related_event_detail'; +import { useResolverQueryParams } from '../use_resolver_query_params'; /** * The team decided to use this table to determine which breadcrumbs/view to display: @@ -145,7 +145,7 @@ const PanelContent = memo(function PanelContent() { */ if (crumbEvent && crumbEvent.length && uiSelectedEvent) { - return 'processEventListNarrowedByType'; + return 'processEventList'; } } @@ -179,9 +179,9 @@ const PanelContent = memo(function PanelContent() { ); } - if (panelToShow === 'processEventListNarrowedByType') { + if (panelToShow === 'processEventList') { return ( - void; -} - -const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)` - flex-flow: row wrap; - display: block; - align-items: baseline; - margin-top: 1em; - - & .euiCallOutHeader { - display: inline; - margin-right: 0.25em; - } - - & .euiText { - display: inline; - } - - & .euiText p { - display: inline; - } -`; - -const DisplayList = memo(function DisplayList({ - crumbs, - matchingEventEntries, - eventType, - processEntityId, -}: { - crumbs: Array<{ text: string | JSX.Element; onClick: () => void }>; - matchingEventEntries: MatchingEventEntry[]; - eventType: string; - processEntityId: string; -}) { - const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId); - const lookupsForThisNode = relatedLookupsByCategory(processEntityId); - const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType); - const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType); - const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType); - - return ( - <> - - {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? ( - - ) : null} - - <> - {matchingEventEntries.map((eventView, index) => { - const { subject, descriptor = '' } = eventView.name; - return ( - - - - - - - - - - - - - - {index === matchingEventEntries.length - 1 ? null : } - - ); - })} - - - ); -}); - -export const ProcessEventListNarrowedByType = memo(function ProcessEventListNarrowedByType({ - processEvent, - eventType, - relatedStats, - pushToQueryParams, -}: { - processEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; - eventType: string; - relatedStats: ResolverNodeStats; -}) { - const processName = processEvent && event.eventName(processEvent); - const processEntityId = event.entityId(processEvent); - const totalCount = Object.values(relatedStats.events.byCategory).reduce( - (sum, val) => sum + val, - 0 - ); - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events', - { - defaultMessage: 'Events', - } - ); - const waitingString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.wait', - { - defaultMessage: 'Waiting For Events...', - } - ); - - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = relatedsReadyMap.get(processEntityId); - - const dispatch = useResolverDispatch(); - - useEffect(() => { - if (typeof relatedsReady === 'undefined') { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); - } - }, [relatedsReady, dispatch, processEntityId]); - - const waitCrumbs = useMemo(() => { - return [ - { - text: eventsString, - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - ]; - }, [pushToQueryParams, eventsString]); - - const relatedByCategory = useSelector(selectors.relatedEventsByCategory); - - /** - * A list entry will be displayed for each of these - */ - const matchingEventEntries: MatchingEventEntry[] = useMemo(() => { - const relateds = relatedByCategory(processEntityId)(eventType).map((resolverEvent) => { - const eventTime = event.eventTimestamp(resolverEvent); - const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); - const entityId = event.eventId(resolverEvent); - - return { - formattedDate, - eventCategory: `${eventType}`, - eventType: `${event.ecsEventType(resolverEvent)}`, - name: event.descriptiveName(resolverEvent), - setQueryParams: () => { - pushToQueryParams({ - crumbId: entityId === undefined ? '' : String(entityId), - crumbEvent: processEntityId, - }); - }, - }; - }); - return relateds; - }, [relatedByCategory, eventType, processEntityId, pushToQueryParams]); - - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - { - text: processName, - onClick: () => { - pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => { - pushToQueryParams({ crumbId: processEntityId, crumbEvent: 'all' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => {}, - }, - ]; - }, [ - eventType, - eventsString, - matchingEventEntries.length, - processEntityId, - processName, - pushToQueryParams, - totalCount, - ]); - - /** - * Wait here until the effect resolves... - */ - if (!relatedsReady) { - return ( - <> - - - -

{waitingString}

-
- - ); - } - - return ( - - ); -}); -ProcessEventListNarrowedByType.displayName = 'ProcessEventListNarrowedByType'; +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo, useEffect, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import styled from 'styled-components'; +import { + CrumbInfo, + formatDate, + StyledBreadcrumbs, + BoldCode, + StyledTime, +} from './panel_content_utilities'; +import * as event from '../../../../common/endpoint/models/event'; +import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; +import * as selectors from '../../store/selectors'; +import { useResolverDispatch } from '../use_resolver_dispatch'; +import { RelatedEventLimitWarning } from '../limit_warnings'; + +/** + * This view presents a list of related events of a given type for a given process. + * It will appear like: + * + * | | + * | :----------------------------------------------------- | + * | **registry deletion** @ *3:32PM..* *HKLM/software...* | + * | **file creation** @ *3:34PM..* *C:/directory/file.exe* | + */ + +interface MatchingEventEntry { + formattedDate: string; + eventType: string; + eventCategory: string; + name: { subject: string; descriptor?: string }; + setQueryParams: () => void; +} + +const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)` + flex-flow: row wrap; + display: block; + align-items: baseline; + margin-top: 1em; + + & .euiCallOutHeader { + display: inline; + margin-right: 0.25em; + } + + & .euiText { + display: inline; + } + + & .euiText p { + display: inline; + } +`; + +const DisplayList = memo(function DisplayList({ + crumbs, + matchingEventEntries, + eventType, + processEntityId, +}: { + crumbs: Array<{ text: string | JSX.Element; onClick: () => void }>; + matchingEventEntries: MatchingEventEntry[]; + eventType: string; + processEntityId: string; +}) { + const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId); + const lookupsForThisNode = relatedLookupsByCategory(processEntityId); + const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType); + const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType); + const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType); + + return ( + <> + + {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? ( + + ) : null} + + <> + {matchingEventEntries.map((eventView, index) => { + const { subject, descriptor = '' } = eventView.name; + return ( + + + + + + + + + + + + + + {index === matchingEventEntries.length - 1 ? null : } + + ); + })} + + + ); +}); + +export const ProcessEventList = memo(function ProcessEventList({ + processEvent, + eventType, + relatedStats, + pushToQueryParams, +}: { + processEvent: ResolverEvent; + pushToQueryParams: (arg0: CrumbInfo) => unknown; + eventType: string; + relatedStats: ResolverNodeStats; +}) { + const processName = processEvent && event.eventName(processEvent); + const processEntityId = event.entityId(processEvent); + const totalCount = Object.values(relatedStats.events.byCategory).reduce( + (sum, val) => sum + val, + 0 + ); + const eventsString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events', + { + defaultMessage: 'Events', + } + ); + const waitingString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.wait', + { + defaultMessage: 'Waiting For Events...', + } + ); + + const relatedsReadyMap = useSelector(selectors.relatedEventsReady); + const relatedsReady = relatedsReadyMap.get(processEntityId); + + const dispatch = useResolverDispatch(); + + useEffect(() => { + if (typeof relatedsReady === 'undefined') { + dispatch({ + type: 'appDetectedMissingEventData', + payload: processEntityId, + }); + } + }, [relatedsReady, dispatch, processEntityId]); + + const waitCrumbs = useMemo(() => { + return [ + { + text: eventsString, + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + ]; + }, [pushToQueryParams, eventsString]); + + const relatedByCategory = useSelector(selectors.relatedEventsByCategory); + + /** + * A list entry will be displayed for each of these + */ + const matchingEventEntries: MatchingEventEntry[] = useMemo(() => { + const relateds = relatedByCategory(processEntityId)(eventType).map((resolverEvent) => { + const eventTime = event.eventTimestamp(resolverEvent); + const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); + const entityId = event.eventId(resolverEvent); + + return { + formattedDate, + eventCategory: `${eventType}`, + eventType: `${event.ecsEventType(resolverEvent)}`, + name: event.descriptiveName(resolverEvent), + setQueryParams: () => { + pushToQueryParams({ + crumbId: entityId === undefined ? '' : String(entityId), + crumbEvent: processEntityId, + }); + }, + }; + }); + return relateds; + }, [relatedByCategory, eventType, processEntityId, pushToQueryParams]); + + const crumbs = useMemo(() => { + return [ + { + text: eventsString, + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + { + text: processName, + onClick: () => { + pushToQueryParams({ crumbId: processEntityId, crumbEvent: '' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => { + pushToQueryParams({ crumbId: processEntityId, crumbEvent: 'all' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => {}, + }, + ]; + }, [ + eventType, + eventsString, + matchingEventEntries.length, + processEntityId, + processName, + pushToQueryParams, + totalCount, + ]); + + /** + * Wait here until the effect resolves... + */ + if (!relatedsReady) { + return ( + <> + + + +

{waitingString}

+
+ + ); + } + + return ( + + ); +}); +ProcessEventList.displayName = 'ProcessEventList'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx index 70422a6919e514..046c8404702624 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx @@ -19,7 +19,7 @@ import * as selectors from '../../store/selectors'; import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { SideEffectContext } from '../side_effect_context'; -import { CubeForProcess } from './process_cube_icon'; +import { CubeForProcess } from './cube_for_process'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index 10e57a09b5da40..3579b1b2f69b88 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -1,375 +1,375 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { memo, useMemo, useEffect, Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; -import { useSelector } from 'react-redux'; -import { FormattedMessage } from 'react-intl'; -import { - CrumbInfo, - formatDate, - StyledBreadcrumbs, - BoldCode, - StyledTime, -} from './panel_content_utilities'; -import * as event from '../../../../common/endpoint/models/event'; -import { ResolverEvent } from '../../../../common/endpoint/types'; -import * as selectors from '../../store/selectors'; -import { useResolverDispatch } from '../use_resolver_dispatch'; -import { PanelContentError } from './panel_content_error'; - -/** - * A helper function to turn objects into EuiDescriptionList entries. - * This reflects the strategy of more or less "dumping" metadata for related processes - * in description lists with little/no 'prettification'. This has the obvious drawback of - * data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields - * to the user "as they occur" in ECS, which may help them with e.g. EQL queries. - * - * Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so: - * {title: "a.b", description: "1"}, {title: "c", description: "d"} - * - * @param {object} obj The object to turn into `
` entries - */ -const objectToDescriptionListEntries = function* ( - obj: object, - prefix = '' -): Generator<{ title: string; description: string }> { - const nextPrefix = prefix.length ? `${prefix}.` : ''; - for (const [metaKey, metaValue] of Object.entries(obj)) { - if (typeof metaValue === 'number' || typeof metaValue === 'string') { - yield { title: nextPrefix + metaKey, description: `${metaValue}` }; - } else if (metaValue instanceof Array) { - yield { - title: nextPrefix + metaKey, - description: metaValue - .filter((arrayEntry) => { - return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; - }) - .join(','), - }; - } else if (typeof metaValue === 'object') { - yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); - } - } -}; - -// Adding some styles to prevent horizontal scrollbars, per request from UX review -const StyledDescriptionList = memo(styled(EuiDescriptionList)` - &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { - max-width: 8em; - } - &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { - max-width: calc(100% - 8.5em); - overflow-wrap: break-word; - } -`); - -// Styling subtitles, per UX review: -const StyledFlexTitle = memo(styled('h3')` - display: flex; - flex-flow: row; - font-size: 1.2em; -`); -const StyledTitleRule = memo(styled('hr')` - &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { - display: block; - flex: 1; - margin-left: 0.5em; - } -`); - -const TitleHr = memo(() => { - return ( - - ); -}); -TitleHr.displayName = 'TitleHR'; - -/** - * This view presents a detailed view of all the available data for a related event, split and titled by the "section" - * it appears in the underlying ResolverEvent - */ -export const RelatedEventDetail = memo(function RelatedEventDetail({ - relatedEventId, - parentEvent, - pushToQueryParams, - countForParent, -}: { - relatedEventId: string; - parentEvent: ResolverEvent; - pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; - countForParent: number | undefined; -}) { - const processName = (parentEvent && event.eventName(parentEvent)) || '*'; - const processEntityId = parentEvent && event.entityId(parentEvent); - const totalCount = countForParent || 0; - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', - { - defaultMessage: 'Events', - } - ); - const naString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA', - { - defaultMessage: 'N/A', - } - ); - - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = relatedsReadyMap.get(processEntityId!); - const dispatch = useResolverDispatch(); - - /** - * If we don't have the related events for the parent yet, use this effect - * to request them. - */ - useEffect(() => { - if (typeof relatedsReady === 'undefined') { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); - } - }, [relatedsReady, dispatch, processEntityId]); - - const relatedEventsForThisProcess = useSelector(selectors.relatedEventsByEntityId).get( - processEntityId! - ); - - const [relatedEventToShowDetailsFor, countBySameCategory, relatedEventCategory] = useMemo(() => { - if (!relatedEventsForThisProcess) { - return [undefined, 0]; - } - const specificEvent = relatedEventsForThisProcess.events.find( - (evt) => event.eventId(evt) === relatedEventId - ); - // For breadcrumbs: - const specificCategory = specificEvent && event.primaryEventCategory(specificEvent); - const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { - return event.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; - }, 0); - return [specificEvent, countOfCategory, specificCategory || naString]; - }, [relatedEventsForThisProcess, naString, relatedEventId]); - - const [sections, formattedDate] = useMemo(() => { - if (!relatedEventToShowDetailsFor) { - // This could happen if user relaods from URL param and requests an eventId that no longer exists - return [[], naString]; - } - // Assuming these details (agent, ecs, process) aren't as helpful, can revisit - const { - agent, - ecs, - process, - ...relevantData - } = relatedEventToShowDetailsFor as ResolverEvent & { - // Type this with various unknown keys so that ts will let us delete those keys - ecs: unknown; - process: unknown; - }; - let displayDate = ''; - const sectionData: Array<{ - sectionTitle: string; - entries: Array<{ title: string; description: string }>; - }> = Object.entries(relevantData) - .map(([sectionTitle, val]) => { - if (sectionTitle === '@timestamp') { - displayDate = formatDate(val); - return { sectionTitle: '', entries: [] }; - } - if (typeof val !== 'object') { - return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; - } - return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; - }) - .filter((v) => v.sectionTitle !== '' && v.entries.length); - return [sectionData, displayDate]; - }, [relatedEventToShowDetailsFor, naString]); - - const waitCrumbs = useMemo(() => { - return [ - { - text: eventsString, - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - ]; - }, [pushToQueryParams, eventsString]); - - const { subject = '', descriptor = '' } = relatedEventToShowDetailsFor - ? event.descriptiveName(relatedEventToShowDetailsFor) - : {}; - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - { - text: processName, - onClick: () => { - pushToQueryParams({ crumbId: processEntityId!, crumbEvent: '' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => { - pushToQueryParams({ crumbId: processEntityId!, crumbEvent: 'all' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => { - pushToQueryParams({ - crumbId: processEntityId!, - crumbEvent: relatedEventCategory || 'all', - }); - }, - }, - { - text: relatedEventToShowDetailsFor ? ( - - ) : ( - naString - ), - onClick: () => {}, - }, - ]; - }, [ - processName, - processEntityId, - eventsString, - pushToQueryParams, - totalCount, - countBySameCategory, - naString, - relatedEventCategory, - relatedEventToShowDetailsFor, - subject, - descriptor, - ]); - - /** - * If the ship hasn't come in yet, wait on the dock - */ - if (!relatedsReady) { - const waitingString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait', - { - defaultMessage: 'Waiting For Events...', - } - ); - return ( - <> - - - -

{waitingString}

-
- - ); - } - - /** - * Could happen if user e.g. loads a URL with a bad crumbEvent - */ - if (!relatedEventToShowDetailsFor) { - const errString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing', - { - defaultMessage: 'Related event not found.', - } - ); - return ( - - ); - } - - return ( - <> - - - - - - - - - - - - - - - - {sections.map(({ sectionTitle, entries }, index) => { - return ( - - {index === 0 ? null : } - - - - {sectionTitle} - - - - - - - {index === sections.length - 1 ? null : } - - ); - })} - - ); -}); +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo, useEffect, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; +import styled from 'styled-components'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { + CrumbInfo, + formatDate, + StyledBreadcrumbs, + BoldCode, + StyledTime, +} from './panel_content_utilities'; +import * as event from '../../../../common/endpoint/models/event'; +import { ResolverEvent } from '../../../../common/endpoint/types'; +import * as selectors from '../../store/selectors'; +import { useResolverDispatch } from '../use_resolver_dispatch'; +import { PanelContentError } from './panel_content_error'; + +/** + * A helper function to turn objects into EuiDescriptionList entries. + * This reflects the strategy of more or less "dumping" metadata for related processes + * in description lists with little/no 'prettification'. This has the obvious drawback of + * data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields + * to the user "as they occur" in ECS, which may help them with e.g. EQL queries. + * + * Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so: + * {title: "a.b", description: "1"}, {title: "c", description: "d"} + * + * @param {object} obj The object to turn into `
` entries + */ +const objectToDescriptionListEntries = function* ( + obj: object, + prefix = '' +): Generator<{ title: string; description: string }> { + const nextPrefix = prefix.length ? `${prefix}.` : ''; + for (const [metaKey, metaValue] of Object.entries(obj)) { + if (typeof metaValue === 'number' || typeof metaValue === 'string') { + yield { title: nextPrefix + metaKey, description: `${metaValue}` }; + } else if (metaValue instanceof Array) { + yield { + title: nextPrefix + metaKey, + description: metaValue + .filter((arrayEntry) => { + return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; + }) + .join(','), + }; + } else if (typeof metaValue === 'object') { + yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); + } + } +}; + +// Adding some styles to prevent horizontal scrollbars, per request from UX review +const StyledDescriptionList = memo(styled(EuiDescriptionList)` + &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { + max-width: 8em; + } + &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { + max-width: calc(100% - 8.5em); + overflow-wrap: break-word; + } +`); + +// Styling subtitles, per UX review: +const StyledFlexTitle = memo(styled('h3')` + display: flex; + flex-flow: row; + font-size: 1.2em; +`); +const StyledTitleRule = memo(styled('hr')` + &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { + display: block; + flex: 1; + margin-left: 0.5em; + } +`); + +const TitleHr = memo(() => { + return ( + + ); +}); +TitleHr.displayName = 'TitleHR'; + +/** + * This view presents a detailed view of all the available data for a related event, split and titled by the "section" + * it appears in the underlying ResolverEvent + */ +export const RelatedEventDetail = memo(function RelatedEventDetail({ + relatedEventId, + parentEvent, + pushToQueryParams, + countForParent, +}: { + relatedEventId: string; + parentEvent: ResolverEvent; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; + countForParent: number | undefined; +}) { + const processName = (parentEvent && event.eventName(parentEvent)) || '*'; + const processEntityId = parentEvent && event.entityId(parentEvent); + const totalCount = countForParent || 0; + const eventsString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', + { + defaultMessage: 'Events', + } + ); + const naString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA', + { + defaultMessage: 'N/A', + } + ); + + const relatedsReadyMap = useSelector(selectors.relatedEventsReady); + const relatedsReady = relatedsReadyMap.get(processEntityId!); + const dispatch = useResolverDispatch(); + + /** + * If we don't have the related events for the parent yet, use this effect + * to request them. + */ + useEffect(() => { + if (typeof relatedsReady === 'undefined') { + dispatch({ + type: 'appDetectedMissingEventData', + payload: processEntityId, + }); + } + }, [relatedsReady, dispatch, processEntityId]); + + const relatedEventsForThisProcess = useSelector(selectors.relatedEventsByEntityId).get( + processEntityId! + ); + + const [relatedEventToShowDetailsFor, countBySameCategory, relatedEventCategory] = useMemo(() => { + if (!relatedEventsForThisProcess) { + return [undefined, 0]; + } + const specificEvent = relatedEventsForThisProcess.events.find( + (evt) => event.eventId(evt) === relatedEventId + ); + // For breadcrumbs: + const specificCategory = specificEvent && event.primaryEventCategory(specificEvent); + const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { + return event.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; + }, 0); + return [specificEvent, countOfCategory, specificCategory || naString]; + }, [relatedEventsForThisProcess, naString, relatedEventId]); + + const [sections, formattedDate] = useMemo(() => { + if (!relatedEventToShowDetailsFor) { + // This could happen if user relaods from URL param and requests an eventId that no longer exists + return [[], naString]; + } + // Assuming these details (agent, ecs, process) aren't as helpful, can revisit + const { + agent, + ecs, + process, + ...relevantData + } = relatedEventToShowDetailsFor as ResolverEvent & { + // Type this with various unknown keys so that ts will let us delete those keys + ecs: unknown; + process: unknown; + }; + let displayDate = ''; + const sectionData: Array<{ + sectionTitle: string; + entries: Array<{ title: string; description: string }>; + }> = Object.entries(relevantData) + .map(([sectionTitle, val]) => { + if (sectionTitle === '@timestamp') { + displayDate = formatDate(val); + return { sectionTitle: '', entries: [] }; + } + if (typeof val !== 'object') { + return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; + } + return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; + }) + .filter((v) => v.sectionTitle !== '' && v.entries.length); + return [sectionData, displayDate]; + }, [relatedEventToShowDetailsFor, naString]); + + const waitCrumbs = useMemo(() => { + return [ + { + text: eventsString, + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + ]; + }, [pushToQueryParams, eventsString]); + + const { subject = '', descriptor = '' } = relatedEventToShowDetailsFor + ? event.descriptiveName(relatedEventToShowDetailsFor) + : {}; + const crumbs = useMemo(() => { + return [ + { + text: eventsString, + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + { + text: processName, + onClick: () => { + pushToQueryParams({ crumbId: processEntityId!, crumbEvent: '' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => { + pushToQueryParams({ crumbId: processEntityId!, crumbEvent: 'all' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => { + pushToQueryParams({ + crumbId: processEntityId!, + crumbEvent: relatedEventCategory || 'all', + }); + }, + }, + { + text: relatedEventToShowDetailsFor ? ( + + ) : ( + naString + ), + onClick: () => {}, + }, + ]; + }, [ + processName, + processEntityId, + eventsString, + pushToQueryParams, + totalCount, + countBySameCategory, + naString, + relatedEventCategory, + relatedEventToShowDetailsFor, + subject, + descriptor, + ]); + + /** + * If the ship hasn't come in yet, wait on the dock + */ + if (!relatedsReady) { + const waitingString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait', + { + defaultMessage: 'Waiting For Events...', + } + ); + return ( + <> + + + +

{waitingString}

+
+ + ); + } + + /** + * Could happen if user e.g. loads a URL with a bad crumbEvent + */ + if (!relatedEventToShowDetailsFor) { + const errString = i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing', + { + defaultMessage: 'Related event not found.', + } + ); + return ( + + ); + } + + return ( + <> + + + + + + + + + + + + + + + + {sections.map(({ sectionTitle, entries }, index) => { + return ( + + {index === 0 ? null : } + + + + {sectionTitle} + + + + + + + {index === sections.length - 1 ? null : } + + ); + })} + + ); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx index 4cdb29b283f1e1..dfc2f970f1e6ff 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import styled from 'styled-components'; -import { Panel } from './panel'; +import { Panel } from './panels'; /** * The top level DOM element for Resolver From ccf8e2b045869f0274534fd6350b896055e8313f Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 6 Aug 2020 22:45:55 +0200 Subject: [PATCH 21/31] [uptime] Ping Redirects (#65292) --- .../uptime/common/runtime_types/ping/ping.ts | 2 +- .../__snapshots__/expanded_row.test.tsx.snap | 26 +++-- .../monitor/ping_list/expanded_row.tsx | 8 +- .../monitor/ping_list/ping_list.tsx | 1 + .../monitor/ping_list/ping_redirects.tsx | 97 +++++++++++++++++++ .../status_bar/monitor_redirects.tsx | 58 +++++++++++ .../status_details/status_bar/status_bar.tsx | 2 + .../__tests__/get_latest_monitor.test.ts | 2 +- .../server/lib/requests/get_latest_monitor.ts | 2 +- .../rest/fixtures/monitor_latest_status.json | 28 +++++- .../functional/apps/uptime/certificates.ts | 2 +- x-pack/test/functional/apps/uptime/index.ts | 4 + .../test/functional/apps/uptime/locations.ts | 5 +- .../functional/apps/uptime/ping_redirects.ts | 74 ++++++++++++++ .../functional/page_objects/uptime_page.ts | 3 +- .../services/uptime/certificates.ts | 8 -- .../test/functional/services/uptime/common.ts | 8 ++ .../functional/services/uptime/monitor.ts | 28 +++++- 18 files changed, 332 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx create mode 100644 x-pack/test/functional/apps/uptime/ping_redirects.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 5ed71acaf77392..0a4d6310927c47 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -143,7 +143,7 @@ export const PingType = t.intersection([ response: t.partial({ body: HttpResponseBodyType, bytes: t.number, - redirects: t.string, + redirects: t.array(t.string), status_code: t.number, }), version: t.string, diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap index 004de391a51a4a..11bdf134bd0e87 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PingListExpandedRow doesn't render list items if the body field is undefined 1`] = ` - + + + + +
+ { }); } return ( - + + {ping?.http?.response?.redirects && ( + + + + )} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index 576810bba24fd2..09782c1b76edb9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -237,6 +237,7 @@ export const PingListComponent = (props: Props) => { render: (item: Ping) => { return ( toggleDetails(item, expandedRows, setExpandedRows)} disabled={!item.error && !(item.http?.response?.body?.bytes ?? 0 > 0)} aria-label={ diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx new file mode 100644 index 00000000000000..b3e59615cbce5e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { EuiListGroup, EuiListGroupItemProps, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { Ping } from '../../../../common/runtime_types/ping'; + +const ListGroup = styled(EuiListGroup)` + &&& { + a { + padding-left: 0; + } + } +`; + +interface Props { + monitorStatus: Ping | null; + showTitle?: boolean; +} + +export const PingRedirects: React.FC = ({ monitorStatus, showTitle }) => { + const monitorUrl = monitorStatus?.url?.full; + + const list = monitorStatus?.http?.response?.redirects; + + const listOfRedirects: EuiListGroupItemProps[] = [ + { + label: monitorUrl, + href: monitorUrl, + iconType: 'globe', + size: 's', + target: '_blank', + extraAction: { + color: 'subdued', + iconType: 'popout', + iconSize: 's', + alwaysShow: true, + 'aria-label': i18n.translate('xpack.uptime.monitorList.redirects.openWindow', { + defaultMessage: 'Link will open in new window.', + }), + }, + }, + ]; + + (list ?? []).forEach((url: string) => { + listOfRedirects.push({ + label: url, + href: url, + iconType: 'sortDown', + size: 's', + target: '_blank', + extraAction: { + color: 'subdued', + iconType: 'popout', + iconSize: 's', + 'aria-label': i18n.translate('xpack.uptime.monitorList.redirects.openWindow', { + defaultMessage: 'Link will open in new window.', + }), + alwaysShow: true, + }, + }); + }); + + const Panel = showTitle ? EuiPanel : 'div'; + + return list ? ( + + {showTitle && ( + +

+ {i18n.translate('xpack.uptime.monitorList.redirects.title', { + defaultMessage: 'Redirects', + })} +

+
+ )} + + { + + {i18n.translate('xpack.uptime.monitorList.redirects.description', { + defaultMessage: 'Heartbeat followed {number} redirects while executing ping.', + values: { + number: list?.length ?? 0, + }, + })} + + } + + +
+ ) : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx new file mode 100644 index 00000000000000..5129db9c2135ba --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover } from '@elastic/eui'; +import styled from 'styled-components'; +import { Ping } from '../../../../../common/runtime_types'; +import { PingRedirects } from '../../ping_list/ping_redirects'; +import { MonListDescription, MonListTitle } from './status_bar'; + +interface Props { + monitorStatus: Ping | null; +} + +const RedirectBtn = styled.span` + cursor: pointer; +`; + +export const MonitorRedirects: React.FC = ({ monitorStatus }) => { + const list = monitorStatus?.http?.response?.redirects; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const button = ( + + setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="uptimeMonitorRedirectInfo" + > + {i18n.translate('xpack.uptime.monitorList.redirects.title.number', { + defaultMessage: '{number}', + values: { + number: list?.length ?? 0, + }, + })} + + + ); + + return list ? ( + <> + Redirects + setIsPopoverOpen(false)} + > + + + + ) : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx index afcc8fae7a8ac0..4ea383567d71c5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx @@ -23,6 +23,7 @@ import { MonitorIDLabel, OverallAvailability } from '../translations'; import { URL_LABEL } from '../../../common/translations'; import { MonitorLocations } from '../../../../../common/runtime_types/monitor'; import { formatAvailabilityValue } from '../availability_reporting/availability_reporting'; +import { MonitorRedirects } from './monitor_redirects'; export const MonListTitle = styled(EuiDescriptionListTitle)` &&& { @@ -76,6 +77,7 @@ export const MonitorStatusBar: React.FC = () => { {MonitorIDLabel} {monitorId} +
); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index 01384ec1452369..669033fc6524ad 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -32,7 +32,7 @@ describe('getLatestMonitor', () => { }, }, size: 1, - _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*'], + _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http'], sort: { '@timestamp': { order: 'desc' }, }, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a58208fc2bb968..3b4aeaf92c5080 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -45,7 +45,7 @@ export const getLatestMonitor: UMElasticsearchQueryFn { }); it('can navigate to cert page', async () => { - await uptimeService.cert.isUptimeDataMissing(); + await uptimeService.common.waitUntilDataIsLoaded(); await uptimeService.cert.hasViewCertButton(); await uptimeService.navigation.goToCertificates(); }); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 6b2b61cba2b648..261f685eeb9ccb 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -56,6 +56,10 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./certificates')); }); + describe('with generated data but no data reset', () => { + loadTestFile(require.resolve('./ping_redirects')); + }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index 8aefca6a70195d..6bfa19c6ef578c 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -40,8 +39,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }; describe('Observer location', () => { - const start = moment().subtract('15', 'm').toISOString(); - const end = moment().toISOString(); + const start = '~ 15 minutes ago'; + const end = 'now'; before(async () => { await addMonitorWithNoLocation(); diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts new file mode 100644 index 00000000000000..b87e8c1748c822 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { uptime: uptimePage, header } = getPageObjects(['uptime', 'header']); + const uptime = getService('uptime'); + const esArchiver = getService('esArchiver'); + + const archive = 'uptime/blank'; + + const monitor = () => uptime.monitor; + + describe('Ping redirects', () => { + const start = '~ 15 minutes ago'; + const end = 'now'; + + const MONITOR_ID = 'redirect-testing-id'; + + before(async () => { + await esArchiver.loadIfNeeded(archive); + }); + + after('unload', async () => { + await esArchiver.unload(archive); + }); + + beforeEach(async () => { + await makeChecksWithStatus( + getService('legacyEs'), + MONITOR_ID, + 5, + 2, + 10000, + { + http: { + rtt: { total: { us: 157784 } }, + response: { + status_code: 200, + redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], + body: { + bytes: 642102, + hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', + }, + }, + }, + }, + 'up' + ); + await delay(1000); + }); + + it('loads and goes to details page', async () => { + await uptime.navigation.goToUptime(); + await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); + }); + + it('display redirect info in detail panel', async () => { + await header.waitUntilLoadingHasFinished(); + await monitor().hasRedirectInfo(); + }); + + it('displays redirects in ping list expand row', async () => { + await monitor().hasRedirectInfoInPingList(); + }); + }); +}; diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 074a2d598be8a0..8102d8b95680e0 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'timePicker']); + const pageObjects = getPageObjects(['common', 'timePicker', 'header']); const { common: commonService, monitor, navigation } = getService('uptime'); const retry = getService('retry'); @@ -42,6 +42,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo } public async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { + await pageObjects.header.waitUntilLoadingHasFinished(); await this.setDateRange(dateStart, dateEnd); await navigation.goToMonitor(monitorId); } diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 06de9be5af7e9f..ab43604786282f 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -24,14 +24,6 @@ export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderCo }; return { - async isUptimeDataMissing() { - return retry.tryForTime(60 * 1000, async () => { - if (await testSubjects.exists('data-missing', { timeout: 0 })) { - await refreshApp(); - } - await testSubjects.missingOrFail('data-missing'); - }); - }, async hasViewCertButton() { return retry.tryForTime(15000, async () => { await testSubjects.existOrFail('uptimeCertificatesLink'); diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 5f544b5e460106..13c9ead89d09d8 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -91,5 +91,13 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) { 5000 ); }, + async waitUntilDataIsLoaded() { + return retry.tryForTime(60 * 1000, async () => { + if (await testSubjects.exists('data-missing')) { + await testSubjects.click('superDatePickerApplyTimeButton'); + } + await testSubjects.missingOrFail('data-missing'); + }); + }, }; } diff --git a/x-pack/test/functional/services/uptime/monitor.ts b/x-pack/test/functional/services/uptime/monitor.ts index 593950fbb7619c..c45454e7316968 100644 --- a/x-pack/test/functional/services/uptime/monitor.ts +++ b/x-pack/test/functional/services/uptime/monitor.ts @@ -7,11 +7,13 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeMonitorProvider({ getService }: FtrProviderContext) { +export function UptimeMonitorProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); + const PageObjects = getPageObjects(['header']); + return { async locationMissingExists() { return await testSubjects.existOrFail('xpack.uptime.locationMap.locationMissing', { @@ -56,5 +58,29 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { async toggleToMapView() { await testSubjects.click('uptimeMonitorToggleMapBtn'); }, + async hasRedirectInfo() { + return retry.tryForTime(30000, async () => { + await testSubjects.existOrFail('uptimeMonitorRedirectInfo'); + }); + }, + async expandPingRow() { + return retry.tryForTime( + 60 * 3000, + async () => { + await testSubjects.existOrFail('uptimePingListExpandBtn', { timeout: 5000 }); + await testSubjects.click('uptimePingListExpandBtn'); + }, + async () => { + await testSubjects.click('superDatePickerApplyTimeButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + ); + }, + async hasRedirectInfoInPingList() { + await this.expandPingRow(); + return retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('uptimeMonitorPingListRedirectInfo'); + }); + }, }; } From 979bdaa56f4aa42f9e962cb803133aaea80d6977 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 6 Aug 2020 19:37:38 -0400 Subject: [PATCH 22/31] [Security Solution][Tech Debt] - Cleans up error formatter to not return duplicate error messages (#74600) ## Summary Using the `formatErrors` util would result in duplicate error messages sometimes. Was noticing this in particular when using union types, where the type validation would check every item in a union and report an error for each one. This resulted in large, repeating errors. Used `uniq` to filter out duplicates. Updated unit tests. --- .../lists/common/schemas/types/comment.test.ts | 3 --- .../schemas/types/default_comments_array.test.ts | 2 -- .../types/default_update_comments_array.test.ts | 2 -- .../lists/common/schemas/types/entries.test.ts | 7 ------- .../types/non_empty_entries_array.test.ts | 4 ---- .../types/non_empty_nested_entries_array.test.ts | 15 --------------- .../common/schemas/types/update_comment.test.ts | 2 -- .../request/create_rules_bulk_schema.test.ts | 1 - .../request/update_rules_bulk_schema.test.ts | 1 - .../common/format_errors.test.ts | 16 ++++++++++++++++ .../security_solution/common/format_errors.ts | 4 +++- .../routes/export_timelines_route.test.ts | 2 +- .../routes/import_timelines_route.test.ts | 10 ++-------- 13 files changed, 22 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts index 081bb9b4bae542..9b6f0e76bdd545 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -60,7 +60,6 @@ describe('Comment', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', - 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', ]); expect(message.schema).toEqual({}); }); @@ -200,7 +199,6 @@ describe('Comment', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); @@ -232,7 +230,6 @@ describe('Comment', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index ee2dc0cf2a478b..0f5ed2ee4a98b8 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -39,7 +39,6 @@ describe('default_comments_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); @@ -51,7 +50,6 @@ describe('default_comments_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 25c84af8c9ee34..a0f6a2b2a6eaa6 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -39,7 +39,6 @@ describe('default_update_comments_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); @@ -51,7 +50,6 @@ describe('default_update_comments_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index f5c022c7a394f4..0537b0b9c6c6a0 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -61,17 +61,10 @@ describe('Entries', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', 'Invalid value "undefined" supplied to "operator"', 'Invalid value "nested" supplied to "type"', 'Invalid value "undefined" supplied to "value"', 'Invalid value "undefined" supplied to "list"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts index 42d476a9fefb28..d81509d0800567 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts @@ -125,10 +125,6 @@ describe('non_empty_entries_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts index 7dbc3465610c00..2e545903689599 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts @@ -86,19 +86,6 @@ describe('non_empty_nested_entries_array', () => { 'Invalid value "undefined" supplied to "operator"', 'Invalid value "nested" supplied to "type"', 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', ]); expect(message.schema).toEqual({}); }); @@ -123,8 +110,6 @@ describe('non_empty_nested_entries_array', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', - 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts index ac4d0304cbb8ea..ba07421fe60f46 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -110,7 +110,6 @@ describe('CommentsUpdate', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); @@ -142,7 +141,6 @@ describe('CommentsUpdate', () => { expect(getPaths(left(message.errors))).toEqual([ 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts index 00854f1ed55262..d335cafdb78853 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -127,7 +127,6 @@ describe('create_rules_bulk_schema', () => { const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([ 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "risk_score"', ]); expect(output.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts index 4cb38889045fce..33a22d9a5f805f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -123,7 +123,6 @@ describe('update_rules_bulk_schema', () => { const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([ 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "risk_score"', ]); expect(output.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/format_errors.test.ts b/x-pack/plugins/security_solution/common/format_errors.test.ts index c8cd72b72816b6..06bdf67097764c 100644 --- a/x-pack/plugins/security_solution/common/format_errors.test.ts +++ b/x-pack/plugins/security_solution/common/format_errors.test.ts @@ -41,6 +41,22 @@ describe('utils', () => { expect(output).toEqual(['some error 1', 'some error 2']); }); + test('it filters out duplicate error messages', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1']); + }); + test('will use message before context if it is set', () => { const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; const validationError1: t.ValidationError = { diff --git a/x-pack/plugins/security_solution/common/format_errors.ts b/x-pack/plugins/security_solution/common/format_errors.ts index ba963f34f2983f..4e1f5e47961523 100644 --- a/x-pack/plugins/security_solution/common/format_errors.ts +++ b/x-pack/plugins/security_solution/common/format_errors.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; export const formatErrors = (errors: t.Errors): string[] => { - return errors.map((error) => { + const err = errors.map((error) => { if (error.message != null) { return error.message; } else { @@ -26,4 +26,6 @@ export const formatErrors = (errors: t.Errors): string[] => { return `Invalid value "${value}" supplied to "${suppliedValue}"`; } }); + + return [...new Set(err)]; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts index a6f0ce232fa7ba..5a976ee7521af4 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts @@ -110,7 +110,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "someId" supplied to "ids",Invalid value "someId" supplied to "ids",Invalid value "{"ids":"someId"}" supplied to "(Partial<{ ids: (Array | null) }> | null)"' + 'Invalid value "someId" supplied to "ids",Invalid value "{"ids":"someId"}" supplied to "(Partial<{ ids: (Array | null) }> | null)"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index 2ad6c5d6fff601..ff76045db90cb8 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -494,10 +494,7 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - [ - 'Invalid value "undefined" supplied to "file"', - 'Invalid value "undefined" supplied to "file"', - ].join(',') + 'Invalid value "undefined" supplied to "file"' ); }); }); @@ -923,10 +920,7 @@ describe('import timeline templates', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - [ - 'Invalid value "undefined" supplied to "file"', - 'Invalid value "undefined" supplied to "file"', - ].join(',') + 'Invalid value "undefined" supplied to "file"' ); }); }); From fbd79ea72677bb4d7aca1c5fc809c2f710e05071 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Aug 2020 19:41:18 -0400 Subject: [PATCH 23/31] skip query of detections page when we do not have .siem-signals index (#74580) * skip query of detections page when we do not have .siem-signals index * review I --- .../timelines/containers/helpers.test.ts | 54 +++++++++++++++++++ .../public/timelines/containers/helpers.ts | 17 ++++++ .../public/timelines/containers/index.tsx | 7 ++- 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/helpers.ts diff --git a/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts new file mode 100644 index 00000000000000..043b5fe39a8bff --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineId } from '../../../common/types/timeline'; +import { skipQueryForDetectionsPage } from './helpers'; + +describe('skipQueryForDetectionsPage', () => { + test('Make sure to NOT skip the query when it is not a timeline from a detection pages', () => { + expect(skipQueryForDetectionsPage(TimelineId.active, ['auditbeat-*', 'filebeat-*'])).toBe( + false + ); + expect( + skipQueryForDetectionsPage(TimelineId.hostsPageEvents, ['auditbeat-*', 'filebeat-*']) + ).toBe(false); + expect( + skipQueryForDetectionsPage(TimelineId.hostsPageExternalAlerts, ['auditbeat-*', 'filebeat-*']) + ).toBe(false); + expect( + skipQueryForDetectionsPage(TimelineId.networkPageExternalAlerts, [ + 'auditbeat-*', + 'filebeat-*', + ]) + ).toBe(false); + }); + + test('Make sure to SKIP the query when it is a timeline from a detection pages without the siem-signals', () => { + expect( + skipQueryForDetectionsPage(TimelineId.detectionsPage, ['auditbeat-*', 'filebeat-*']) + ).toBe(true); + expect( + skipQueryForDetectionsPage(TimelineId.detectionsRulesDetailsPage, [ + 'auditbeat-*', + 'filebeat-*', + ]) + ).toBe(true); + }); + + test('Make sure to NOT skip the query when it is a timeline from a detection pages with the siem-signals', () => { + expect( + skipQueryForDetectionsPage(TimelineId.detectionsPage, [ + 'auditbeat-*', + '.siem-signals-rainbow-butterfly', + ]) + ).toBe(false); + expect( + skipQueryForDetectionsPage(TimelineId.detectionsRulesDetailsPage, [ + '.siem-signals-rainbow-butterfly', + ]) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts new file mode 100644 index 00000000000000..aef6f4df6f41bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineId } from '../../../common/types/timeline'; + +export const detectionsTimelineIds = [ + TimelineId.detectionsPage, + TimelineId.detectionsRulesDetailsPage, +]; + +export const skipQueryForDetectionsPage = (id: string, defaultIndex: string[]) => + id != null && + detectionsTimelineIds.some((timelineId) => timelineId === id) && + !defaultIndex.some((di) => di.toLowerCase().startsWith('.siem-signals')); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 562999108b4b0a..de7175f0a7f97f 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -11,7 +11,6 @@ import { Query } from 'react-apollo'; import { compose, Dispatch } from 'redux'; import { connect, ConnectedProps } from 'react-redux'; -import { TimelineId } from '../../../common/types/timeline'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; import { @@ -28,8 +27,7 @@ import { QueryTemplate, QueryTemplateProps } from '../../common/containers/query import { EventType } from '../../timelines/store/timeline/model'; import { timelineQuery } from './index.gql_query'; import { timelineActions } from '../../timelines/store/timeline'; - -const timelineIds = [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage]; +import { detectionsTimelineIds, skipQueryForDetectionsPage } from './helpers'; export interface TimelineArgs { events: TimelineItem[]; @@ -130,6 +128,7 @@ class TimelineQueryComponent extends QueryTemplate< query={timelineQuery} fetchPolicy="network-only" notifyOnNetworkStatusChange + skip={skipQueryForDetectionsPage(id, defaultIndex)} variables={variables} > {({ data, loading, fetchMore, refetch }) => { @@ -202,7 +201,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch) => ({ clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && timelineIds.some((timelineId) => timelineId === id)) { + if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { dispatch(timelineActions.clearEventsLoading({ id })); dispatch(timelineActions.clearEventsDeleted({ id })); } From 5d9f329a36d8be7fe8601498beadb1e8f7cfd072 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 7 Aug 2020 08:03:13 -0500 Subject: [PATCH 24/31] [Ingest Manager] Integration tests for updating a package (#74593) * add integration tests for updating a package's assets * update to update tests and change to dataset to data_stream * add datastream test --- .../services/epm/elasticsearch/ilm/install.ts | 1 - .../apis/epm/data_stream.ts | 130 ++++++++ .../apis/epm/index.js | 2 + .../apis/epm/install_remove_assets.ts | 5 +- .../apis/epm/update_assets.ts | 299 ++++++++++++++++++ .../0.1.0/dataset/test_logs/fields/ecs.yml | 3 + .../0.1.0/dataset/test_metrics/fields/ecs.yml | 3 + .../visualization/sample_visualization.json | 2 +- .../elasticsearch/ilm_policy/all_assets.json | 15 + .../elasticsearch/ingest_pipeline/default.yml | 7 + .../0.2.0/dataset/test_logs/fields/ecs.yml | 6 + .../0.2.0/dataset/test_logs/fields/fields.yml | 16 + .../0.2.0/dataset/test_logs/manifest.yml | 9 + .../0.2.0/dataset/test_logs2/fields/ecs.yml | 3 + .../dataset/test_logs2/fields/fields.yml | 16 + .../0.2.0/dataset/test_logs2/manifest.yml | 3 + .../0.2.0/dataset/test_metrics/fields/ecs.yml | 3 + .../dataset/test_metrics/fields/fields.yml | 16 + .../0.2.0/dataset/test_metrics/manifest.yml | 3 + .../all_assets/0.2.0/docs/README.md | 3 + .../0.2.0/img/logo_overrides_64_color.svg | 7 + .../kibana/dashboard/sample_dashboard.json | 16 + .../0.2.0/kibana/search/sample_search2.json | 24 ++ .../visualization/sample_visualization.json | 11 + .../all_assets/0.2.0/manifest.yml | 20 ++ .../elasticsearch/ilm_policy/all_assets.json | 15 + .../elasticsearch/ingest_pipeline/default.yml | 7 + .../0.1.0/dataset/test_logs/fields/ecs.yml | 3 + .../0.1.0/dataset/test_logs/fields/fields.yml | 16 + .../0.1.0/dataset/test_logs/manifest.yml | 9 + .../0.1.0/dataset/test_metrics/fields/ecs.yml | 3 + .../dataset/test_metrics/fields/fields.yml | 16 + .../0.1.0/dataset/test_metrics/manifest.yml | 3 + .../datastreams/0.1.0/docs/README.md | 3 + .../datastreams/0.1.0/manifest.yml | 20 ++ .../elasticsearch/ilm_policy/all_assets.json | 15 + .../elasticsearch/ingest_pipeline/default.yml | 7 + .../0.2.0/dataset/test_logs/fields/ecs.yml | 6 + .../0.2.0/dataset/test_logs/fields/fields.yml | 16 + .../0.2.0/dataset/test_logs/manifest.yml | 9 + .../0.2.0/dataset/test_metrics/fields/ecs.yml | 6 + .../dataset/test_metrics/fields/fields.yml | 16 + .../0.2.0/dataset/test_metrics/manifest.yml | 3 + .../datastreams/0.2.0/docs/README.md | 3 + .../datastreams/0.2.0/manifest.yml | 20 ++ 45 files changed, 816 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/search/sample_search2.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/visualization/sample_visualization.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/ecs.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts index 9590167657d987..c5253e4902cabd 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts @@ -16,7 +16,6 @@ export async function installILMPolicy(paths: string[], callCluster: CallESAsCur const { file } = Registry.pathParts(path); const name = file.substr(0, file.lastIndexOf('.')); try { - if (await policyExists(name, callCluster)) return; await callCluster('transport.request', { method: 'PUT', path: '/_ilm/policy/' + name, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts new file mode 100644 index 00000000000000..68a4812d4af404 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + const pkgName = 'datastreams'; + const pkgVersion = '0.1.0'; + const pkgUpdateVersion = '0.2.0'; + const pkgKey = `${pkgName}-${pkgVersion}`; + const pkgUpdateKey = `${pkgName}-${pkgUpdateVersion}`; + const logsTemplateName = `logs-${pkgName}.test_logs`; + const metricsTemplateName = `metrics-${pkgName}.test_metrics`; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest + .post(`/api/ingest_manager/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + describe('datastreams', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await installPackage(pkgKey); + await es.transport.request({ + method: 'POST', + path: `/${logsTemplateName}-default/_doc`, + body: { + '@timestamp': '2015-01-01', + logs_test_name: 'test', + data_stream: { + dataset: `${pkgName}.test_logs`, + namespace: 'default', + type: 'logs', + }, + }, + }); + await es.transport.request({ + method: 'POST', + path: `/${metricsTemplateName}-default/_doc`, + body: { + '@timestamp': '2015-01-01', + logs_test_name: 'test', + data_stream: { + dataset: `${pkgName}.test_metrics`, + namespace: 'default', + type: 'metrics', + }, + }, + }); + }); + after(async () => { + await uninstallPackage(pkgUpdateKey); + await es.transport.request({ + method: 'DELETE', + path: `/_data_stream/${logsTemplateName}-default`, + }); + await es.transport.request({ + method: 'DELETE', + path: `/_data_stream/${metricsTemplateName}-default`, + }); + }); + describe('get datastreams after data sent', async () => { + skipIfNoDockerRegistry(providerContext); + let resLogsDatastream: any; + let resMetricsDatastream: any; + before(async () => { + resLogsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${logsTemplateName}-default`, + }); + resMetricsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${metricsTemplateName}-default`, + }); + }); + it('should list the logs datastream', async function () { + expect(resLogsDatastream.body.data_streams.length).equal(1); + expect(resLogsDatastream.body.data_streams[0].indices.length).equal(1); + expect(resLogsDatastream.body.data_streams[0].indices[0].index_name).equal( + `.ds-${logsTemplateName}-default-000001` + ); + }); + it('should list the metrics datastream', async function () { + expect(resMetricsDatastream.body.data_streams.length).equal(1); + expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); + expect(resMetricsDatastream.body.data_streams[0].indices[0].index_name).equal( + `.ds-${metricsTemplateName}-default-000001` + ); + }); + }); + describe('rollover datastream when mappings are not compatible', async () => { + skipIfNoDockerRegistry(providerContext); + let resLogsDatastream: any; + let resMetricsDatastream: any; + before(async () => { + await installPackage(pkgUpdateKey); + resLogsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${logsTemplateName}-default`, + }); + resMetricsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${metricsTemplateName}-default`, + }); + }); + it('should have rolled over logs datastream', async function () { + expect(resLogsDatastream.body.data_streams[0].indices.length).equal(2); + expect(resLogsDatastream.body.data_streams[0].indices[1].index_name).equal( + `.ds-${logsTemplateName}-default-000002` + ); + }); + it('should have not rolled over metrics datastream', async function () { + expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 1582f72dd1cd82..0f32d2b4ae7039 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -13,5 +13,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_remove_assets')); loadTestFile(require.resolve('./install_update')); + loadTestFile(require.resolve('./update_assets')); + loadTestFile(require.resolve('./data_stream')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 35058de0684b21..03d0b6abb4802b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -23,7 +23,10 @@ export default function (providerContext: FtrProviderContext) { await supertest.delete(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); }; const installPackage = async (pkg: string) => { - await supertest.post(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + await supertest + .post(`/api/ingest_manager/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); }; describe('installs and uninstalls all assets', async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts new file mode 100644 index 00000000000000..59ad7a9744ae1f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -0,0 +1,299 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const es = getService('es'); + const pkgName = 'all_assets'; + const pkgVersion = '0.1.0'; + const pkgUpdateVersion = '0.2.0'; + const pkgKey = `${pkgName}-${pkgVersion}`; + const pkgUpdateKey = `${pkgName}-${pkgUpdateVersion}`; + const logsTemplateName = `logs-${pkgName}.test_logs`; + const logsTemplateName2 = `logs-${pkgName}.test_logs2`; + const metricsTemplateName = `metrics-${pkgName}.test_metrics`; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest + .post(`/api/ingest_manager/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + describe('updates all assets when updating a package to a different version', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await installPackage(pkgKey); + await installPackage(pkgUpdateKey); + }); + after(async () => { + await uninstallPackage(pkgUpdateKey); + }); + it('should have updated the ILM policy', async function () { + const resPolicy = await es.transport.request({ + method: 'GET', + path: `/_ilm/policy/all_assets`, + }); + expect(resPolicy.body.all_assets.policy).eql({ + phases: { + hot: { + min_age: '1ms', + actions: { + rollover: { + max_size: '50gb', + max_age: '31d', + }, + }, + }, + }, + }); + }); + it('should have updated the index templates', async function () { + const resLogsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${logsTemplateName}`, + }); + expect(resLogsTemplate.statusCode).equal(200); + expect( + resLogsTemplate.body.index_templates[0].index_template.template.mappings.properties + ).eql({ + '@timestamp': { + type: 'date', + }, + logs_test_name: { + type: 'text', + }, + new_field_name: { + ignore_above: 1024, + type: 'keyword', + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + }, + namespace: { + type: 'constant_keyword', + }, + type: { + type: 'constant_keyword', + }, + }, + }, + }); + const resMetricsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${metricsTemplateName}`, + }); + expect(resMetricsTemplate.statusCode).equal(200); + expect( + resMetricsTemplate.body.index_templates[0].index_template.template.mappings.properties + ).eql({ + '@timestamp': { + type: 'date', + }, + metrics_test_name2: { + ignore_above: 1024, + type: 'keyword', + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + }, + namespace: { + type: 'constant_keyword', + }, + type: { + type: 'constant_keyword', + }, + }, + }, + }); + }); + it('should have installed the new index template', async function () { + const resLogsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${logsTemplateName2}`, + }); + expect(resLogsTemplate.statusCode).equal(200); + expect( + resLogsTemplate.body.index_templates[0].index_template.template.mappings.properties + ).eql({ + '@timestamp': { + type: 'date', + }, + test_logs2: { + ignore_above: 1024, + type: 'keyword', + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + }, + namespace: { + type: 'constant_keyword', + }, + type: { + type: 'constant_keyword', + }, + }, + }, + }); + }); + it('should have installed the new versionized pipeline', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgUpdateVersion}`, + }); + expect(res.statusCode).equal(200); + }); + it('should have removed the old versionized pipelines', async function () { + let res; + try { + res = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, + }); + } catch (err) { + res = err; + } + expect(res.statusCode).equal(404); + }); + it('should have updated the template components', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-mappings`, + }); + expect(res.statusCode).equal(200); + expect(res.body.component_templates[0].component_template.template.mappings).eql({ + dynamic: true, + properties: { '@timestamp': { type: 'date' } }, + }); + const resSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-settings`, + }); + expect(res.statusCode).equal(200); + expect(resSettings.body.component_templates[0].component_template.template.settings).eql({ + index: { lifecycle: { name: 'reference2' } }, + }); + }); + it('should have updated the index patterns', async function () { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + const updated = fields.filter((field: { name: string }) => field.name === 'new_field_name'); + expect(!!updated.length).equal(true); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + const updatedMetrics = fieldsMetrics.filter( + (field: { name: string }) => field.name === 'metrics_test_name2' + ); + expect(!!updatedMetrics.length).equal(true); + }); + it('should have updated the kibana assets', async function () { + const resDashboard = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard', + }); + expect(resDashboard.id).equal('sample_dashboard'); + let resDashboard2; + try { + resDashboard2 = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard2', + }); + } catch (err) { + resDashboard2 = err; + } + expect(resDashboard2.response.data.statusCode).equal(404); + const resVis = await kibanaServer.savedObjects.get({ + type: 'visualization', + id: 'sample_visualization', + }); + expect(resVis.attributes.description).equal('sample visualization 0.2.0'); + let resSearch; + try { + resSearch = await kibanaServer.savedObjects.get({ + type: 'search', + id: 'sample_search', + }); + } catch (err) { + resSearch = err; + } + expect(resSearch.response.data.statusCode).equal(404); + const resSearch2 = await kibanaServer.savedObjects.get({ + type: 'search', + id: 'sample_search2', + }); + expect(resSearch2.id).equal('sample_search2'); + }); + it('should have updated the saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + expect(res.attributes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_search2', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs-0.2.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'logs-all_assets.test_logs2', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.2.0', + internal: false, + removable: true, + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/ecs.yml new file mode 100644 index 00000000000000..3d88fe5dfefb60 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_test_name + title: logs_test_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/ecs.yml new file mode 100644 index 00000000000000..a30e3c7a878560 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_test_name + title: metrics_test_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json index e814b83bbf3242..917479fd7d120b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json @@ -1,6 +1,6 @@ { "attributes": { - "description": "sample visualization", + "description": "sample visualization update", "title": "sample vis title", "uiStateJSON": "{}", "version": 1, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json new file mode 100644 index 00000000000000..d8bab8a75f680b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "1ms", + "actions": { + "rollover": { + "max_size": "50gb", + "max_age": "31d" + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 00000000000000..580db049d0d5d1 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,7 @@ +--- +description: Pipeline for parsing test logs + plugins. +processors: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/ecs.yml new file mode 100644 index 00000000000000..7df52cc11fd205 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/manifest.yml new file mode 100644 index 00000000000000..8a53f9e26e827a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: true + index_template.settings: + index.lifecycle.name: reference2 \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/ecs.yml new file mode 100644 index 00000000000000..c5819deb1ee371 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: test_logs2 + title: test_logs2 + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/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/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/manifest.yml new file mode 100644 index 00000000000000..e12f454657ea27 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs2/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: logs \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/ecs.yml new file mode 100644 index 00000000000000..9529c3a8eaf1a6 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_test_name2 + title: metrics_test_title2 + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/manifest.yml new file mode 100644 index 00000000000000..6bc20442bd4327 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/docs/README.md new file mode 100644 index 00000000000000..2617f1fcabe11c --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing that a package installs its elasticsearch assets when installed for the first time (not updating) and removing the package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/img/logo_overrides_64_color.svg new file mode 100644 index 00000000000000..b03007a76ffcc5 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json new file mode 100644 index 00000000000000..ef08d693242104 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "description": "Sample dashboard", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false}", + "panelsJSON": "[{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"panelRefName\":\"panel_0\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"kafka.log.class\",\"kafka.log.trace.class\",\"kafka.log.trace.full\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":12,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"panelRefName\":\"panel_1\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"log.level\",\"kafka.log.component\",\"message\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":20,\"i\":\"3\",\"w\":48,\"x\":0,\"y\":20},\"panelIndex\":\"3\",\"panelRefName\":\"panel_2\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"4\",\"w\":48,\"x\":0,\"y\":12},\"panelIndex\":\"4\",\"panelRefName\":\"panel_3\",\"version\":\"7.3.0\"}]", + "timeRestore": false, + "title": "[Logs Sample] Overview ECS", + "version": 1 + }, + "id": "sample_dashboard", + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/search/sample_search2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/search/sample_search2.json new file mode 100644 index 00000000000000..aa5cea19208a42 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/search/sample_search2.json @@ -0,0 +1,24 @@ +{ + "attributes": { + "columns": [ + "log.level", + "kafka.log.component", + "message" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\",\"key\":\"dataset.name\",\"negate\":false,\"params\":{\"query\":\"kafka.log\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"log\"},\"query\":{\"match\":{\"dataset.name\":{\"query\":\"kafka.log\",\"type\":\"phrase\"}}}}],\"highlightAll\":true,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "All logs [Logs Kafka] ECS", + "version": 1 + }, + "id": "sample_search2", + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/visualization/sample_visualization.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/visualization/sample_visualization.json new file mode 100644 index 00000000000000..626f1f787f4216 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/visualization/sample_visualization.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "description": "sample visualization 0.2.0", + "title": "sample vis title", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "sample_visualization", + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/manifest.yml new file mode 100644 index 00000000000000..70da51a14bce83 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: all_assets +title: All Assets Updated +description: tests that all assets are updated +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json new file mode 100644 index 00000000000000..7cf62e890f865c --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb", + "max_age": "30d" + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 00000000000000..580db049d0d5d1 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,7 @@ +--- +description: Pipeline for parsing test logs + plugins. +processors: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/ecs.yml new file mode 100644 index 00000000000000..3d88fe5dfefb60 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_test_name + title: logs_test_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/manifest.yml new file mode 100644 index 00000000000000..8cd522e2845bbc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/ecs.yml new file mode 100644 index 00000000000000..a30e3c7a878560 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_test_name + title: metrics_test_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/manifest.yml new file mode 100644 index 00000000000000..6bc20442bd4327 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/dataset/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/docs/README.md new file mode 100644 index 00000000000000..34b1f08a55cbef --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing that datastream rolls over when mappings are not compatible diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/manifest.yml new file mode 100644 index 00000000000000..0ab43760b7ee87 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: datastreams +title: datastream test +description: This is a test package for testing that datastreams rollover when mappings are incompatible +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json new file mode 100644 index 00000000000000..d8bab8a75f680b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "1ms", + "actions": { + "rollover": { + "max_size": "50gb", + "max_age": "31d" + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 00000000000000..580db049d0d5d1 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,7 @@ +--- +description: Pipeline for parsing test logs + plugins. +processors: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/ecs.yml new file mode 100644 index 00000000000000..7df52cc11fd205 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/manifest.yml new file mode 100644 index 00000000000000..8a53f9e26e827a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: true + index_template.settings: + index.lifecycle.name: reference2 \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/ecs.yml new file mode 100644 index 00000000000000..8fb3ccd3de8fdc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/fields/fields.yml new file mode 100644 index 00000000000000..6e003ed0ad1476 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/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/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/manifest.yml new file mode 100644 index 00000000000000..6bc20442bd4327 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/dataset/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/docs/README.md new file mode 100644 index 00000000000000..34b1f08a55cbef --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing that datastream rolls over when mappings are not compatible diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/manifest.yml new file mode 100644 index 00000000000000..1aa1410bd0aefc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: datastreams +title: datastream test +description: This is a test package for testing that datastreams rollover when mappings are incompatible +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' From 7dc33f9ba8f93873dcd15509da125587c1730bb4 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Fri, 7 Aug 2020 09:15:35 -0400 Subject: [PATCH 25/31] [Resolver] UI tests for the panel and bug fix (#74421) * Change the way the resolver simulator works * refactor resolver tree and data access layer mocks * Fix bug where timestamp and pid sometimes don't show in the node detail view * add a few tests for the panel (not done, but worth committing.) --- .../common/endpoint/types.ts | 9 + ...ildren.ts => no_ancestors_two_children.ts} | 48 ++--- ..._children_with_related_events_on_origin.ts | 94 +++++++++ .../{store => }/mocks/endpoint_event.ts | 21 +- .../{store => }/mocks/resolver_tree.ts | 48 +++-- .../resolver/store/data/selectors.test.ts | 2 +- .../resolver/store/mocks/related_event.ts | 36 ---- .../public/resolver/store/selectors.test.ts | 2 +- .../test_utilities/simulator/index.tsx | 194 ++++++++---------- .../resolver/view/clickthrough.test.tsx | 81 ++++---- .../public/resolver/view/panel.test.tsx | 59 ++++++ .../resolver/view/panels/process_details.tsx | 26 ++- .../view/panels/process_list_with_counts.tsx | 7 +- 13 files changed, 390 insertions(+), 237 deletions(-) rename x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/{one_ancestor_two_children.ts => no_ancestors_two_children.ts} (62%) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts rename x-pack/plugins/security_solution/public/resolver/{store => }/mocks/endpoint_event.ts (66%) rename x-pack/plugins/security_solution/public/resolver/{store => }/mocks/resolver_tree.ts (89%) delete mode 100644 x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 61ce672405fd58..ffde47825b501b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -182,6 +182,15 @@ export interface ResolverRelatedEvents { nextEvent: string | null; } +/** + * Safe version of `ResolverRelatedEvents` + */ +export interface SafeResolverRelatedEvents { + entityID: string; + events: SafeResolverEvent[]; + nextEvent: string | null; +} + /** * Response structure for the alerts route. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts similarity index 62% rename from x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts rename to x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index 94c176d343d177..b0407fa5d7c1d1 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -9,11 +9,8 @@ import { ResolverTree, ResolverEntityIndex, } from '../../../../common/endpoint/types'; -import { mockEndpointEvent } from '../../store/mocks/endpoint_event'; -import { - mockTreeWithNoAncestorsAnd2Children, - withRelatedEventsOnOrigin, -} from '../../store/mocks/resolver_tree'; +import { mockEndpointEvent } from '../../mocks/endpoint_event'; +import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; interface Metadata { @@ -43,24 +40,11 @@ interface Metadata { /** * A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored. */ -export function oneAncestorTwoChildren( - { withRelatedEvents }: { withRelatedEvents: Iterable<[string, string]> | null } = { - withRelatedEvents: null, - } -): { dataAccessLayer: DataAccessLayer; metadata: Metadata } { +export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } { const metadata: Metadata = { databaseDocumentID: '_id', entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, }; - const baseTree = mockTreeWithNoAncestorsAnd2Children({ - originID: metadata.entityIDs.origin, - firstChildID: metadata.entityIDs.firstChild, - secondChildID: metadata.entityIDs.secondChild, - }); - const composedTree = withRelatedEvents - ? withRelatedEventsOnOrigin(baseTree, withRelatedEvents) - : baseTree; - return { metadata, dataAccessLayer: { @@ -70,17 +54,13 @@ export function oneAncestorTwoChildren( relatedEvents(entityID: string): Promise { return Promise.resolve({ entityID, - events: - /* Respond with the mocked related events when the origin's related events are fetched*/ withRelatedEvents && - entityID === metadata.entityIDs.origin - ? composedTree.relatedEvents.events - : [ - mockEndpointEvent({ - entityID, - name: 'event', - timestamp: 0, - }), - ], + events: [ + mockEndpointEvent({ + entityID, + name: 'event', + timestamp: 0, + }), + ], nextEvent: null, }); }, @@ -89,7 +69,13 @@ export function oneAncestorTwoChildren( * Fetch a ResolverTree for a entityID */ resolverTree(): Promise { - return Promise.resolve(composedTree); + return Promise.resolve( + mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }) + ); }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts new file mode 100644 index 00000000000000..01e75e3eefdbfa --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataAccessLayer } from '../../types'; +import { mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin } from '../../mocks/resolver_tree'; +import { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: { + /** + * The entityID of the node related to the document being analyzed. + */ + origin: 'origin'; + /** + * The entityID of the first child of the origin. + */ + firstChild: 'firstChild'; + /** + * The entityID of the second child of the origin. + */ + secondChild: 'secondChild'; + }; +} + +export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + const metadata: Metadata = { + databaseDocumentID: '_id', + entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, + }; + const tree = mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }); + + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + relatedEvents(entityID: string): Promise { + /** + * Respond with the mocked related events when the origin's related events are fetched. + **/ + const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; + + return Promise.resolve({ + entityID, + events, + nextEvent: null, + } as ResolverRelatedEvents); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + resolverTree(): Promise { + return Promise.resolve(tree); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(): string[] { + return ['index pattern']; + }, + + /** + * Get entities matching a document. + */ + entities(): Promise { + return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts similarity index 66% rename from x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts rename to x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts index 709f2faf13b006..c822fdf647c169 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent } from '../../../../common/endpoint/types'; +import { EndpointEvent } from '../../../common/endpoint/types'; /** * Simple mock endpoint event that works for tree layouts. @@ -28,10 +28,29 @@ export function mockEndpointEvent({ type: lifecycleType ? lifecycleType : 'start', category: 'process', }, + agent: { + id: 'agent.id', + version: 'agent.version', + type: 'agent.type', + }, + ecs: { + version: 'ecs.version', + }, + user: { + name: 'user.name', + domain: 'user.domain', + }, process: { entity_id: entityID, + executable: 'executable', + args: 'args', name, + pid: 0, + hash: { + md5: 'hash.md5', + }, parent: { + pid: 0, entity_id: parentEntityId, }, }, diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts similarity index 89% rename from x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts rename to x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 21d0309501aa88..5d2cbb2eab0dc2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -5,8 +5,7 @@ */ import { mockEndpointEvent } from './endpoint_event'; -import { mockRelatedEvent } from './related_event'; -import { ResolverTree, ResolverEvent } from '../../../../common/endpoint/types'; +import { ResolverTree, ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; export function mockTreeWith2AncestorsAndNoChildren({ originID, @@ -125,11 +124,11 @@ type RelatedEventType = string; * @param treeToAddRelatedEventsTo the ResolverTree to modify * @param relatedEventsToAddByCategoryAndType Iterable of `[category, type]` pairs describing related events. e.g. [['dns','info'],['registry','access']] */ -export function withRelatedEventsOnOrigin( +function withRelatedEventsOnOrigin( treeToAddRelatedEventsTo: ResolverTree, relatedEventsToAddByCategoryAndType: Iterable<[RelatedEventCategory, RelatedEventType]> ): ResolverTree { - const events = []; + const events: SafeResolverEvent[] = []; const byCategory: Record = {}; const stats = { totalAlerts: 0, @@ -139,14 +138,18 @@ export function withRelatedEventsOnOrigin( }, }; for (const [category, type] of relatedEventsToAddByCategoryAndType) { - events.push( - mockRelatedEvent({ - entityID: treeToAddRelatedEventsTo.entityID, - timestamp: 1, - category, + events.push({ + '@timestamp': 1, + event: { + kind: 'event', type, - }) - ); + category, + id: 'xyz', + }, + process: { + entity_id: treeToAddRelatedEventsTo.entityID, + }, + }); stats.events.total++; stats.events.byCategory[category] = stats.events.byCategory[category] ? stats.events.byCategory[category] + 1 @@ -156,7 +159,7 @@ export function withRelatedEventsOnOrigin( ...treeToAddRelatedEventsTo, stats, relatedEvents: { - events, + events: events as ResolverEvent[], nextEvent: null, }, }; @@ -309,3 +312,24 @@ export function mockTreeWithNoProcessEvents(): ResolverTree { }, }; } + +export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ + originID, + firstChildID, + secondChildID, +}: { + originID: string; + firstChildID: string; + secondChildID: string; +}) { + const baseTree = mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, + }); + const withRelatedEvents: Array<[string, string]> = [ + ['registry', 'access'], + ['registry', 'access'], + ]; + return withRelatedEventsOnOrigin(baseTree, withRelatedEvents); +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 6786a93f1d9cac..15a981d4607301 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -15,7 +15,7 @@ import { mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents, mockTreeWithAllProcessesTerminated, mockTreeWithNoProcessEvents, -} from '../mocks/resolver_tree'; +} from '../../mocks/resolver_tree'; import { uniquePidForProcess } from '../../models/process_event'; import { EndpointEvent } from '../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts deleted file mode 100644 index 1e0c460a3a711d..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { EndpointEvent } from '../../../../common/endpoint/types'; - -/** - * Simple mock related event. - */ -export function mockRelatedEvent({ - entityID, - timestamp, - category, - type, - id, -}: { - entityID: string; - timestamp: number; - category: string; - type: string; - id?: string; -}): EndpointEvent { - return { - '@timestamp': timestamp, - event: { - kind: 'event', - type, - category, - id: id ?? 'xyz', - }, - process: { - entity_id: entityID, - }, - } as EndpointEvent; -} diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index dfbc6bd290686f..f113e861d3ce94 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -12,7 +12,7 @@ import * as selectors from './selectors'; import { mockTreeWith2AncestorsAndNoChildren, mockTreeWithNoAncestorsAnd2Children, -} from './mocks/resolver_tree'; +} from '../mocks/resolver_tree'; import { SafeResolverEvent } from '../../../common/endpoint/types'; describe('resolver selectors', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index ed30643ed871e4..6f44c5aee7cac8 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -113,83 +113,21 @@ export class Simulator { } /** - * Return a promise that resolves after the `store`'s next state transition. - * Used by `mapStateTransitions` + * Yield the result of `mapper` over and over, once per event-loop cycle. + * After 10 times, quit. + * Use this to continually check a value. See `toYieldEqualTo`. */ - private stateTransitioned(): Promise { - // keep track of the resolve function of the promise that has been returned. - let resolveState: (() => void) | null = null; - - const promise: Promise = new Promise((resolve) => { - // Immediately expose the resolve function in the outer scope. It will be resolved when the next state transition occurs. - resolveState = resolve; - }); - - // Subscribe to the store - const unsubscribe = this.store.subscribe(() => { - // Once a state transition occurs, unsubscribe. - unsubscribe(); - // Resolve the promise. The null assertion is safe here as Promise initializers run immediately (according to spec and node/browser implementations.) - // NB: the state is not resolved here. Code using the simulator should not rely on state or selectors of state. - resolveState!(); - }); - - // Return the promise that will be resolved on the next state transition, allowing code to `await` for the next state transition. - return promise; - } - - /** - * This will yield the return value of `mapper` after each state transition. If no state transition occurs for 10 event loops in a row, this will give up. - */ - public async *mapStateTransitions(mapper: () => R): AsyncIterable { - // Yield the value before any state transitions have occurred. - yield mapper(); - - /** Increment this each time an event loop completes without a state transition. - * If this value hits `10`, end the loop. - * - * Code will test assertions after each state transition. If the assertion hasn't passed and no further state transitions occur, - * then the jest timeout will happen. The timeout doesn't give a useful message about the assertion. - * By short-circuiting this function, code that uses it can short circuit the test timeout and print a useful error message. - * - * NB: the logic to short-circuit the loop is here because knowledge of state is a concern of the simulator, not tests. - */ + public async *map(mapper: () => R): AsyncIterable { let timeoutCount = 0; - while (true) { - /** - * `await` a race between the next state transition and a timeout that happens after `0`ms. - * If the timeout wins, no `dispatch` call caused a state transition in the last loop. - * If this keeps happening, assume that Resolver isn't going to do anything else. - * - * If Resolver adds intentional delay logic (e.g. waiting before making a request), this code might have to change. - * In that case, Resolver should use the side effect context to schedule future work. This code could then subscribe to some event published by the side effect context. That way, this code will be aware of Resolver's intention to do work. - */ - const timedOut: boolean = await Promise.race([ - (async (): Promise => { - await this.stateTransitioned(); - // If a state transition occurs, return false for `timedOut` - return false; - })(), - new Promise((resolve) => { - setTimeout(() => { - // If a timeout occurs, resolve `timedOut` as true - return resolve(true); - }, 0); - }), - ]); - - if (timedOut) { - // If a timout occurred, note it. - timeoutCount++; - if (timeoutCount === 10) { - // if 10 timeouts happen in a row, end the loop early - return; - } - } else { - // If a state transition occurs, reset the timeout count and yield the value - timeoutCount = 0; - yield mapper(); - } + while (timeoutCount < 10) { + timeoutCount++; + yield mapper(); + await new Promise((resolve) => { + setTimeout(() => { + this.wrapper.update(); + resolve(); + }, 0); + }); } } @@ -198,25 +136,22 @@ export class Simulator { * returns a `ReactWrapper` even if nothing is found, as that is how `enzyme` does things. */ public processNodeElements(options: ProcessNodeElementSelectorOptions = {}): ReactWrapper { - return this.findInDOM(processNodeElementSelector(options)); + return this.domNodes(processNodeElementSelector(options)); } /** - * true if a process node element is found for the entityID and if it has an [aria-selected] attribute. + * Return the node element with the given `entityID`. */ - public processNodeElementLooksSelected(entityID: string): boolean { - return this.processNodeElements({ entityID, selected: true }).length === 1; + public selectedProcessNode(entityID: string): ReactWrapper { + return this.processNodeElements({ entityID, selected: true }); } /** - * true if a process node element is found for the entityID and if it *does not have* an [aria-selected] attribute. + * Return the node element with the given `entityID`. It will only be returned if it is not selected. */ - public processNodeElementLooksUnselected(entityID: string): boolean { - // find the process node, then exclude it if its selected. - return ( - this.processNodeElements({ entityID }).not( - processNodeElementSelector({ entityID, selected: true }) - ).length === 1 + public unselectedProcessNode(entityID: string): ReactWrapper { + return this.processNodeElements({ entityID }).not( + processNodeElementSelector({ entityID, selected: true }) ); } @@ -234,11 +169,8 @@ export class Simulator { * @param entityID The entity ID of the proocess node to select in */ public processNodeRelatedEventButton(entityID: string): ReactWrapper { - return this.processNodeElements({ entityID }).findWhere( - (wrapper) => - // Filter out React components - typeof wrapper.type() === 'string' && - wrapper.prop('data-test-subj') === 'resolver:submenu:button' + return this.domNodes( + `${processNodeElementSelector({ entityID })} [data-test-subj="resolver:submenu:button"]` ); } @@ -256,42 +188,98 @@ export class Simulator { * The element that shows when Resolver is waiting for the graph data. */ public graphLoadingElement(): ReactWrapper { - return this.findInDOM('[data-test-subj="resolver:graph:loading"]'); + return this.domNodes('[data-test-subj="resolver:graph:loading"]'); } /** * The element that shows if Resolver couldn't draw the graph. */ public graphErrorElement(): ReactWrapper { - return this.findInDOM('[data-test-subj="resolver:graph:error"]'); + return this.domNodes('[data-test-subj="resolver:graph:error"]'); } /** * The element where nodes get drawn. */ public graphElement(): ReactWrapper { - return this.findInDOM('[data-test-subj="resolver:graph"]'); + return this.domNodes('[data-test-subj="resolver:graph"]'); + } + + /** + * An element with a list of all nodes. + */ + public nodeListElement(): ReactWrapper { + return this.domNodes('[data-test-subj="resolver:node-list"]'); + } + + /** + * Return the items in the node list (the default panel view.) + */ + public nodeListItems(): ReactWrapper { + return this.domNodes('[data-test-subj="resolver:node-list:item"]'); + } + + /** + * The element containing the details for the selected node. + */ + public nodeDetailElement(): ReactWrapper { + return this.domNodes('[data-test-subj="resolver:node-detail"]'); + } + + /** + * The details of the selected node are shown in a description list. This returns the title elements of the description list. + */ + private nodeDetailEntryTitle(): ReactWrapper { + return this.domNodes('[data-test-subj="resolver:node-detail:entry-title"]'); } /** - * The outer panel container. + * The details of the selected node are shown in a description list. This returns the description elements of the description list. */ - public panelElement(): ReactWrapper { - return this.findInDOM('[data-test-subj="resolver:panel"]'); + private nodeDetailEntryDescription(): ReactWrapper { + return this.domNodes('[data-test-subj="resolver:node-detail:entry-description"]'); } /** - * The panel content element (which may include tables, lists, other data depending on the view). + * Return DOM nodes that match `enzymeSelector`. */ - public panelContentElement(): ReactWrapper { - return this.findInDOM('[data-test-subj^="resolver:panel:"]'); + private domNodes(enzymeSelector: string): ReactWrapper { + return this.wrapper + .find(enzymeSelector) + .filterWhere((wrapper) => typeof wrapper.type() === 'string'); } /** - * Like `this.wrapper.find` but only returns DOM nodes. + * The titles and descriptions (as text) from the node detail panel. */ - private findInDOM(selector: string): ReactWrapper { - return this.wrapper.find(selector).filterWhere((wrapper) => typeof wrapper.type() === 'string'); + public nodeDetailDescriptionListEntries(): Array<[string, string]> { + const titles = this.nodeDetailEntryTitle(); + const descriptions = this.nodeDetailEntryDescription(); + const entries: Array<[string, string]> = []; + for (let index = 0; index < Math.min(titles.length, descriptions.length); index++) { + const title = titles.at(index).text(); + const description = descriptions.at(index).text(); + + // Exclude timestamp since we can't currently calculate the expected description for it from tests + if (title !== '@timestamp') { + entries.push([title, description]); + } + } + return entries; + } + + /** + * Resolve the wrapper returned by `wrapperFactory` only once it has at least 1 element in it. + */ + public async resolveWrapper( + wrapperFactory: () => ReactWrapper, + predicate: (wrapper: ReactWrapper) => boolean = (wrapper) => wrapper.length > 0 + ): Promise { + for await (const wrapper of this.map(wrapperFactory)) { + if (predicate(wrapper)) { + return wrapper; + } + } } } diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index c819491dd28f0d..98ea235d3524fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { oneAncestorTwoChildren } from '../data_access_layer/mocks/one_ancestor_two_children'; +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher import '../test_utilities/extend_jest'; +import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; let simulator: Simulator; let databaseDocumentID: string; @@ -16,10 +17,10 @@ let entityIDs: { origin: string; firstChild: string; secondChild: string }; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', () => { +describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { beforeEach(async () => { // create a mock data access layer - const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneAncestorTwoChildren(); + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); // save a reference to the entity IDs exposed by the mock data layer entityIDs = dataAccessLayerMetadata.entityIDs; @@ -40,7 +41,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( * * For example, there might be no loading element at one point, and 1 graph element at one point, but never a single time when there is both 1 graph element and 0 loading elements. */ - simulator.mapStateTransitions(() => ({ + simulator.map(() => ({ graphElements: simulator.graphElement().length, graphLoadingElements: simulator.graphLoadingElement().length, graphErrorElements: simulator.graphErrorElement().length, @@ -55,22 +56,23 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( // Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow. it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected.`, async () => { - expect(simulator.processNodeElementLooksSelected(entityIDs.origin)).toBe(true); - - expect(simulator.processNodeElementLooksUnselected(entityIDs.firstChild)).toBe(true); - expect(simulator.processNodeElementLooksUnselected(entityIDs.secondChild)).toBe(true); - - expect(simulator.processNodeElements().length).toBe(3); + await expect( + simulator.map(() => ({ + selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length, + unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length, + unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length, + processNodeCount: simulator.processNodeElements().length, + })) + ).toYieldEqualTo({ + selectedOriginCount: 1, + unselectedFirstChildCount: 1, + unselectedSecondChildCount: 1, + processNodeCount: 3, + }); }); - it(`should have the default "process list" panel present`, async () => { - expect(simulator.panelElement().length).toBe(1); - expect(simulator.panelContentElement().length).toBe(1); - const testSubjectName = simulator - .panelContentElement() - .getDOMNode() - .getAttribute('data-test-subj'); - expect(testSubjectName).toMatch(/process-list/g); + it(`should show the node list`, async () => { + await expect(simulator.map(() => simulator.nodeListElement().length)).toYieldEqualTo(1); }); describe("when the second child node's first button has been clicked", () => { @@ -82,42 +84,37 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( .first() .simulate('click'); }); - it('should render the second child node as selected, and the first child not as not selected, and the query string should indicate that the second child is selected', async () => { + it('should render the second child node as selected, and the origin as not selected, and the query string should indicate that the second child is selected', async () => { await expect( - simulator.mapStateTransitions(function value() { - return { - // the query string has a key showing that the second child is selected - queryStringSelectedNode: simulator.queryStringValues().selectedNode, - // the second child is rendered in the DOM, and shows up as selected - secondChildLooksSelected: simulator.processNodeElementLooksSelected( - entityIDs.secondChild - ), - // the origin is in the DOM, but shows up as unselected - originLooksUnselected: simulator.processNodeElementLooksUnselected(entityIDs.origin), - }; - }) + simulator.map(() => ({ + // the query string has a key showing that the second child is selected + queryStringSelectedNode: simulator.queryStringValues().selectedNode, + // the second child is rendered in the DOM, and shows up as selected + selectedSecondChildNodeCount: simulator.selectedProcessNode(entityIDs.secondChild) + .length, + // the origin is in the DOM, but shows up as unselected + unselectedOriginNodeCount: simulator.unselectedProcessNode(entityIDs.origin).length, + })) ).toYieldEqualTo({ // Just the second child should be marked as selected in the query string queryStringSelectedNode: [entityIDs.secondChild], // The second child is rendered and has `[aria-selected]` - secondChildLooksSelected: true, + selectedSecondChildNodeCount: 1, // The origin child is rendered and doesn't have `[aria-selected]` - originLooksUnselected: true, + unselectedOriginNodeCount: 1, }); }); }); }); }); -describe('Resolver, when analyzing a tree that has some related events', () => { +describe('Resolver, when analyzing a tree that has two related events for the origin', () => { beforeEach(async () => { // create a mock data access layer with related events - const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneAncestorTwoChildren({ - withRelatedEvents: [ - ['registry', 'access'], - ['registry', 'access'], - ], - }); + const { + metadata: dataAccessLayerMetadata, + dataAccessLayer, + } = noAncestorsTwoChildrenWithRelatedEventsOnOrigin(); // save a reference to the entity IDs exposed by the mock data layer entityIDs = dataAccessLayerMetadata.entityIDs; @@ -132,7 +129,7 @@ describe('Resolver, when analyzing a tree that has some related events', () => { describe('when it has loaded', () => { beforeEach(async () => { await expect( - simulator.mapStateTransitions(() => ({ + simulator.map(() => ({ graphElements: simulator.graphElement().length, graphLoadingElements: simulator.graphLoadingElement().length, graphErrorElements: simulator.graphErrorElement().length, @@ -148,7 +145,7 @@ describe('Resolver, when analyzing a tree that has some related events', () => { it('should render a related events button', async () => { await expect( - simulator.mapStateTransitions(() => ({ + simulator.map(() => ({ relatedEventButtons: simulator.processNodeRelatedEventButton(entityIDs.origin).length, })) ).toYieldEqualTo({ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx new file mode 100644 index 00000000000000..78e5fd79bea134 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { Simulator } from '../test_utilities/simulator'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; + +describe('Resolver: when analyzing a tree with no ancestors and two children', () => { + let simulator: Simulator; + let databaseDocumentID: string; + + // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances + const resolverComponentInstanceID = 'resolverComponentInstanceID'; + + beforeEach(async () => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + }); + + it('should show the node list', async () => { + await expect(simulator.map(() => simulator.nodeListElement().length)).toYieldEqualTo(1); + }); + + it('should have 3 nodes in the node list', async () => { + await expect(simulator.map(() => simulator.nodeListItems().length)).toYieldEqualTo(3); + }); + describe('when there is an item in the node list and it has been clicked', () => { + beforeEach(async () => { + const nodeListItems = await simulator.resolveWrapper(() => simulator.nodeListItems()); + expect(nodeListItems && nodeListItems.length).toBeTruthy(); + if (nodeListItems) { + nodeListItems.first().find('button').simulate('click'); + } + }); + it('should show the details for the first node', async () => { + await expect( + simulator.map(() => simulator.nodeDetailDescriptionListEntries()) + ).toYieldEqualTo([ + ['process.executable', 'executable'], + ['process.pid', '0'], + ['user.name', 'user.name'], + ['user.domain', 'user.domain'], + ['process.parent.pid', '0'], + ['process.hash.md5', 'hash.md5'], + ['process.args', 'args'], + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index 03d9e4c2d5a2b3..112a3400c4947a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, HTMLAttributes } from 'react'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from 'react-intl'; +import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import * as selectors from '../../store/selectors'; import * as event from '../../../../common/endpoint/models/event'; import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities'; @@ -51,9 +52,9 @@ export const ProcessDetails = memo(function ProcessDetails({ const processName = event.eventName(processEvent); const entityId = event.entityId(processEvent); const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); - const processInfoEntry = useMemo(() => { + const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); - const dateTime = eventTime ? formatDate(eventTime) : ''; + const dateTime = eventTime === undefined ? null : formatDate(eventTime); const createdEntry = { title: '@timestamp', @@ -95,7 +96,7 @@ export const ProcessDetails = memo(function ProcessDetails({ description: argsForProcess(processEvent), }; - // This is the data in {title, description} form for the EUIDescriptionList to display + // This is the data in {title, description} form for the EuiDescriptionList to display const processDescriptionListData = [ createdEntry, pathEntry, @@ -107,7 +108,7 @@ export const ProcessDetails = memo(function ProcessDetails({ commandLineEntry, ] .filter((entry) => { - return entry.description; + return entry.description !== undefined; }) .map((entry) => { return { @@ -172,13 +173,24 @@ export const ProcessDetails = memo(function ProcessDetails({ + } + descriptionProps={ + { 'data-test-subj': 'resolver:node-detail:entry-description' } as HTMLAttributes< + HTMLElement + > + } compressed listItems={processInfoEntry} /> ); }); -ProcessDetails.displayName = 'ProcessDetails'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx index 046c8404702624..11f005f8acbcd9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx @@ -150,7 +150,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ const processTableView: ProcessTableView[] = useMemo( () => [...processNodePositions.keys()].map((processEvent) => { - let dateTime; + let dateTime: Date | undefined; const eventTime = event.timestampSafeVersion(processEvent); const name = event.processNameSafeVersion(processEvent); if (eventTime) { @@ -186,13 +186,15 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ const children = useSelector(selectors.hasMoreChildren); const ancestors = useSelector(selectors.hasMoreAncestors); const showWarning = children === true || ancestors === true; + const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( <> {showWarning && } - data-test-subj="resolver:panel:process-list" + rowProps={rowProps} + data-test-subj="resolver:node-list" items={processTableView} columns={columns} sorting @@ -200,4 +202,3 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ ); }); -ProcessListWithCounts.displayName = 'ProcessListWithCounts'; From c6c300e8f82d138f28a080a7a34a78ebc27d9776 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 7 Aug 2020 11:21:44 -0400 Subject: [PATCH 26/31] [Canvas][tech-debt] Add Typescript to apps directory (#73766) Co-authored-by: Elastic Machine --- .../export/__tests__/export_app.test.tsx | 8 +- .../export/export/export_app.component.tsx | 63 ++++++++++++++ .../public/apps/export/export/export_app.js | 59 ------------- .../public/apps/export/export/export_app.ts | 21 +++++ .../canvas/public/apps/export/export/index.js | 30 ------- .../export/index.ts} | 5 +- .../public/apps/export/{index.js => index.ts} | 0 .../apps/export/{routes.js => routes.ts} | 13 ++- .../{home_app.js => home_app.component.tsx} | 10 ++- .../home/home_app/{index.js => home_app.ts} | 8 +- .../home_app/index.ts} | 5 +- .../public/apps/home/{index.js => index.ts} | 0 .../public/apps/home/{routes.js => routes.ts} | 0 .../canvas/public/apps/{index.js => index.ts} | 1 + .../apps/workpad/{index.js => index.ts} | 0 .../apps/workpad/{routes.js => routes.ts} | 24 ++++-- .../public/apps/workpad/workpad_app/index.js | 41 --------- .../public/apps/workpad/workpad_app/index.ts | 8 ++ .../workpad_app/workpad_app.component.tsx | 83 +++++++++++++++++++ .../apps/workpad/workpad_app/workpad_app.js | 81 ------------------ .../apps/workpad/workpad_app/workpad_app.ts | 32 +++++++ .../workpad_header.component.tsx | 12 ++- .../workpad_header/workpad_header.tsx | 29 +------ x-pack/plugins/canvas/types/canvas.ts | 4 + 24 files changed, 274 insertions(+), 263 deletions(-) create mode 100644 x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx delete mode 100644 x-pack/plugins/canvas/public/apps/export/export/export_app.js create mode 100644 x-pack/plugins/canvas/public/apps/export/export/export_app.ts delete mode 100644 x-pack/plugins/canvas/public/apps/export/export/index.js rename x-pack/plugins/canvas/public/apps/{workpad/workpad_app/load_workpad.js => export/export/index.ts} (67%) rename x-pack/plugins/canvas/public/apps/export/{index.js => index.ts} (100%) rename x-pack/plugins/canvas/public/apps/export/{routes.js => routes.ts} (79%) rename x-pack/plugins/canvas/public/apps/home/home_app/{home_app.js => home_app.component.tsx} (79%) rename x-pack/plugins/canvas/public/apps/home/home_app/{index.js => home_app.ts} (69%) rename x-pack/plugins/canvas/public/apps/{export/export/load_workpad.js => home/home_app/index.ts} (69%) rename x-pack/plugins/canvas/public/apps/home/{index.js => index.ts} (100%) rename x-pack/plugins/canvas/public/apps/home/{routes.js => routes.ts} (100%) rename x-pack/plugins/canvas/public/apps/{index.js => index.ts} (88%) rename x-pack/plugins/canvas/public/apps/workpad/{index.js => index.ts} (100%) rename x-pack/plugins/canvas/public/apps/workpad/{routes.js => routes.ts} (82%) delete mode 100644 x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js create mode 100644 x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts create mode 100644 x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx delete mode 100644 x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js create mode 100644 x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx index b0a8d1e990e758..1bb58919b7fa65 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx +++ b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { mount } from 'enzyme'; -// @ts-expect-error untyped local -import { ExportApp } from '../export_app'; +import { ExportApp } from '../export_app.component'; +import { CanvasWorkpad } from '../../../../../types'; jest.mock('style-it', () => ({ it: (css: string, Component: any) => Component, @@ -23,7 +23,7 @@ jest.mock('../../../../components/link', () => ({ describe('', () => { test('renders as expected', () => { - const sampleWorkpad = { + const sampleWorkpad = ({ id: 'my-workpad-abcd', css: '', pages: [ @@ -34,7 +34,7 @@ describe('', () => { elements: [3, 4, 5, 6], }, ], - }; + } as any) as CanvasWorkpad; const page1 = mount( {}} /> diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx new file mode 100644 index 00000000000000..03121e749d0dc4 --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect } from 'react'; +import PropTypes from 'prop-types'; +// @ts-expect-error untyped library +import Style from 'style-it'; +// @ts-expect-error untyped local +import { WorkpadPage } from '../../../components/workpad_page'; +import { Link } from '../../../components/link'; +import { CanvasWorkpad } from '../../../../types'; + +interface Props { + workpad: CanvasWorkpad; + selectedPageIndex: number; + initializeWorkpad: () => void; +} + +export const ExportApp: FC = ({ workpad, selectedPageIndex, initializeWorkpad }) => { + const { id, pages, height, width } = workpad; + const activePage = pages[selectedPageIndex]; + const pageElementCount = activePage.elements.length; + + useEffect(() => initializeWorkpad()); + + return ( +
+
+
+ + Edit Workpad + +
+ {Style.it( + workpad.css, +
+ {}} + unregisterLayout={() => {}} + /> +
+ )} +
+
+ ); +}; + +ExportApp.propTypes = { + workpad: PropTypes.shape({ + id: PropTypes.string.isRequired, + pages: PropTypes.array.isRequired, + }).isRequired, + selectedPageIndex: PropTypes.number.isRequired, + initializeWorkpad: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.js b/x-pack/plugins/canvas/public/apps/export/export/export_app.js deleted file mode 100644 index 1d02d85cae0b31..00000000000000 --- a/x-pack/plugins/canvas/public/apps/export/export/export_app.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import Style from 'style-it'; -import { WorkpadPage } from '../../../components/workpad_page'; -import { Link } from '../../../components/link'; - -export class ExportApp extends React.PureComponent { - static propTypes = { - workpad: PropTypes.shape({ - id: PropTypes.string.isRequired, - pages: PropTypes.array.isRequired, - }).isRequired, - selectedPageIndex: PropTypes.number.isRequired, - initializeWorkpad: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.props.initializeWorkpad(); - } - - render() { - const { workpad, selectedPageIndex } = this.props; - const { pages, height, width } = workpad; - const activePage = pages[selectedPageIndex]; - const pageElementCount = activePage.elements.length; - - return ( -
-
-
- - Edit Workpad - -
- {Style.it( - workpad.css, -
- {}} - unregisterLayout={() => {}} - /> -
- )} -
-
- ); - } -} diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.ts b/x-pack/plugins/canvas/public/apps/export/export/export_app.ts new file mode 100644 index 00000000000000..b47d1950ec2b75 --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { initializeWorkpad } from '../../../state/actions/workpad'; +import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad'; +import { ExportApp as Component } from './export_app.component'; +import { State } from '../../../../types'; + +export const ExportApp = connect( + (state: State) => ({ + workpad: getWorkpad(state), + selectedPageIndex: getSelectedPageIndex(state), + }), + (dispatch) => ({ + initializeWorkpad: () => dispatch(initializeWorkpad()), + }) +)(Component); diff --git a/x-pack/plugins/canvas/public/apps/export/export/index.js b/x-pack/plugins/canvas/public/apps/export/export/index.js deleted file mode 100644 index 95c46d9e1c8ae5..00000000000000 --- a/x-pack/plugins/canvas/public/apps/export/export/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { compose, branch, renderComponent } from 'recompose'; -import { initializeWorkpad } from '../../../state/actions/workpad'; -import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad'; -import { LoadWorkpad } from './load_workpad'; -import { ExportApp as Component } from './export_app'; - -const mapStateToProps = (state) => ({ - workpad: getWorkpad(state), - selectedPageIndex: getSelectedPageIndex(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - initializeWorkpad() { - dispatch(initializeWorkpad()); - }, -}); - -const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))]; - -export const ExportApp = compose( - connect(mapStateToProps, mapDispatchToProps), - ...branches -)(Component); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js b/x-pack/plugins/canvas/public/apps/export/export/index.ts similarity index 67% rename from x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js rename to x-pack/plugins/canvas/public/apps/export/export/index.ts index 388bf00723f82c..81939d550a7ab1 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js +++ b/x-pack/plugins/canvas/public/apps/export/export/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -export const LoadWorkpad = () =>
Load a workpad...
; +export { ExportApp } from './export_app'; +export { ExportApp as ExportAppComponent } from './export_app.component'; diff --git a/x-pack/plugins/canvas/public/apps/export/index.js b/x-pack/plugins/canvas/public/apps/export/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/export/index.js rename to x-pack/plugins/canvas/public/apps/export/index.ts diff --git a/x-pack/plugins/canvas/public/apps/export/routes.js b/x-pack/plugins/canvas/public/apps/export/routes.ts similarity index 79% rename from x-pack/plugins/canvas/public/apps/export/routes.js rename to x-pack/plugins/canvas/public/apps/export/routes.ts index 33e375115aa19d..0b4f74149fb4f8 100644 --- a/x-pack/plugins/canvas/public/apps/export/routes.js +++ b/x-pack/plugins/canvas/public/apps/export/routes.ts @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Dispatch } from 'redux'; +// @ts-expect-error Untyped local import * as workpadService from '../../lib/workpad_service'; import { setWorkpad } from '../../state/actions/workpad'; +// @ts-expect-error Untyped local import { fetchAllRenderables } from '../../state/actions/elements'; +// @ts-expect-error Untyped local import { setPage } from '../../state/actions/pages'; +// @ts-expect-error Untyped local import { setAssets } from '../../state/actions/assets'; import { ExportApp } from './export'; @@ -18,7 +23,13 @@ export const routes = [ { name: 'exportWorkpad', path: '/pdf/:id/page/:page', - action: (dispatch) => async ({ params, router }) => { + action: (dispatch: Dispatch) => async ({ + params, + // @ts-expect-error Fix when Router is typed. + router, + }: { + params: { id: string; page: string }; + }) => { // load workpad if given a new id via url param const fetchedWorkpad = await workpadService.get(params.id); const pageNumber = parseInt(params.page, 10); diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.js b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx similarity index 79% rename from x-pack/plugins/canvas/public/apps/home/home_app/home_app.js rename to x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx index bfa4abbf7c56d8..3c2e989cc8e51c 100644 --- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.js +++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx @@ -4,12 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +// @ts-expect-error untyped local import { WorkpadManager } from '../../../components/workpad_manager'; +// @ts-expect-error untyped local import { setDocTitle } from '../../../lib/doc_title'; -export const HomeApp = ({ onLoad = () => {} }) => { +interface Props { + onLoad: () => void; +} + +export const HomeApp: FC = ({ onLoad = () => {} }) => { onLoad(); setDocTitle('Canvas'); return ( diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/index.js b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts similarity index 69% rename from x-pack/plugins/canvas/public/apps/home/home_app/index.js rename to x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts index f78ee1f8a18af5..ff9d1c1cc63ac0 100644 --- a/x-pack/plugins/canvas/public/apps/home/home_app/index.js +++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts @@ -6,12 +6,10 @@ import { connect } from 'react-redux'; import { resetWorkpad } from '../../../state/actions/workpad'; -import { HomeApp as Component } from './home_app'; +import { HomeApp as Component } from './home_app.component'; -const mapDispatchToProps = (dispatch) => ({ +export const HomeApp = connect(null, (dispatch) => ({ onLoad() { dispatch(resetWorkpad()); }, -}); - -export const HomeApp = connect(null, mapDispatchToProps)(Component); +}))(Component); diff --git a/x-pack/plugins/canvas/public/apps/export/export/load_workpad.js b/x-pack/plugins/canvas/public/apps/home/home_app/index.ts similarity index 69% rename from x-pack/plugins/canvas/public/apps/export/export/load_workpad.js rename to x-pack/plugins/canvas/public/apps/home/home_app/index.ts index 388bf00723f82c..8ea92312e3e501 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/load_workpad.js +++ b/x-pack/plugins/canvas/public/apps/home/home_app/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -export const LoadWorkpad = () =>
Load a workpad...
; +export { HomeApp } from './home_app'; +export { HomeApp as HomeAppComponent } from './home_app.component'; diff --git a/x-pack/plugins/canvas/public/apps/home/index.js b/x-pack/plugins/canvas/public/apps/home/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/home/index.js rename to x-pack/plugins/canvas/public/apps/home/index.ts diff --git a/x-pack/plugins/canvas/public/apps/home/routes.js b/x-pack/plugins/canvas/public/apps/home/routes.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/home/routes.js rename to x-pack/plugins/canvas/public/apps/home/routes.ts diff --git a/x-pack/plugins/canvas/public/apps/index.js b/x-pack/plugins/canvas/public/apps/index.ts similarity index 88% rename from x-pack/plugins/canvas/public/apps/index.js rename to x-pack/plugins/canvas/public/apps/index.ts index c014349ca18dad..8b3d378e23f809 100644 --- a/x-pack/plugins/canvas/public/apps/index.js +++ b/x-pack/plugins/canvas/public/apps/index.ts @@ -8,6 +8,7 @@ import * as home from './home'; import * as workpad from './workpad'; import * as exp from './export'; +// @ts-expect-error Router and routes are not yet strongly typed export const routes = [].concat(workpad.routes, home.routes, exp.routes); export const apps = [workpad.WorkpadApp, home.HomeApp, exp.ExportApp]; diff --git a/x-pack/plugins/canvas/public/apps/workpad/index.js b/x-pack/plugins/canvas/public/apps/workpad/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/workpad/index.js rename to x-pack/plugins/canvas/public/apps/workpad/index.ts diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.js b/x-pack/plugins/canvas/public/apps/workpad/routes.ts similarity index 82% rename from x-pack/plugins/canvas/public/apps/workpad/routes.js rename to x-pack/plugins/canvas/public/apps/workpad/routes.ts index a330020b741ac1..d83f85f7173058 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/plugins/canvas/public/apps/workpad/routes.ts @@ -4,17 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ErrorStrings } from '../../../i18n'; +import { Dispatch } from 'redux'; +// @ts-expect-error import * as workpadService from '../../lib/workpad_service'; import { notifyService } from '../../services'; import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; +// @ts-expect-error import { getDefaultWorkpad } from '../../state/defaults'; import { setWorkpad } from '../../state/actions/workpad'; +// @ts-expect-error import { setAssets, resetAssets } from '../../state/actions/assets'; +// @ts-expect-error import { setPage } from '../../state/actions/pages'; import { getWorkpad } from '../../state/selectors/workpad'; +// @ts-expect-error import { setZoomScale } from '../../state/actions/transient'; +import { ErrorStrings } from '../../../i18n'; import { WorkpadApp } from './workpad_app'; +import { State } from '../../../types'; const { workpadRoutes: strings } = ErrorStrings; @@ -25,7 +32,8 @@ export const routes = [ { name: 'createWorkpad', path: '/create', - action: (dispatch) => async ({ router }) => { + // @ts-expect-error Fix when Router is typed. + action: (dispatch: Dispatch) => async ({ router }) => { const newWorkpad = getDefaultWorkpad(); try { await workpadService.create(newWorkpad); @@ -46,7 +54,13 @@ export const routes = [ { name: 'loadWorkpad', path: '/:id(/page/:page)', - action: (dispatch, getState) => async ({ params, router }) => { + action: (dispatch: Dispatch, getState: () => State) => async ({ + params, + // @ts-expect-error Fix when Router is typed. + router, + }: { + params: { id: string; page?: string }; + }) => { // load workpad if given a new id via url param const state = getState(); const currentWorkpad = getWorkpad(state); @@ -70,10 +84,10 @@ export const routes = [ // fetch the workpad again, to get changes const workpad = getWorkpad(getState()); - const pageNumber = parseInt(params.page, 10); + const pageNumber = params.page ? parseInt(params.page, 10) : null; // no page provided, append current page to url - if (isNaN(pageNumber)) { + if (!pageNumber || isNaN(pageNumber)) { return router.redirectTo('loadWorkpad', { id: workpad.id, page: workpad.page + 1 }); } diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js deleted file mode 100644 index ac50cd3fb99b62..00000000000000 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { compose, branch, renderComponent } from 'recompose'; -import { selectToplevelNodes } from '../../../state/actions/transient'; -import { canUserWrite, getAppReady } from '../../../state/selectors/app'; -import { getWorkpad, isWriteable } from '../../../state/selectors/workpad'; -import { LoadWorkpad } from './load_workpad'; -import { WorkpadApp as Component } from './workpad_app'; -import { withElementsLoadedTelemetry } from './workpad_telemetry'; - -export { WORKPAD_CONTAINER_ID } from './workpad_app'; - -const mapStateToProps = (state) => { - const appReady = getAppReady(state); - - return { - isWriteable: isWriteable(state) && canUserWrite(state), - appReady: typeof appReady === 'object' ? appReady : { ready: appReady }, - workpad: getWorkpad(state), - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - deselectElement(ev) { - ev && ev.stopPropagation(); - dispatch(selectToplevelNodes([])); - }, -}); - -const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))]; - -export const WorkpadApp = compose( - connect(mapStateToProps, mapDispatchToProps), - ...branches, - withElementsLoadedTelemetry -)(Component); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts new file mode 100644 index 00000000000000..a00bf855ba3763 --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkpadApp } from './workpad_app'; +export { WorkpadApp as WorkpadAppComponent } from './workpad_app.component'; diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx new file mode 100644 index 00000000000000..791f40f0219cdb --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, MouseEventHandler, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { Sidebar } from '../../../components/sidebar'; +import { Toolbar } from '../../../components/toolbar'; +// @ts-expect-error Untyped local +import { Workpad } from '../../../components/workpad'; +import { WorkpadHeader } from '../../../components/workpad_header'; +import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants'; +import { CommitFn } from '../../../../types'; + +export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer'; + +interface Props { + deselectElement?: MouseEventHandler; + isWriteable: boolean; +} + +export const WorkpadApp: FC = ({ deselectElement, isWriteable }) => { + const interactivePageLayout = useRef(null); // future versions may enable editing on multiple pages => use array then + + const registerLayout = (newLayout: CommitFn) => { + if (interactivePageLayout.current !== newLayout) { + interactivePageLayout.current = newLayout; + } + }; + + const unregisterLayout = (oldLayout: CommitFn) => { + if (interactivePageLayout.current === oldLayout) { + interactivePageLayout.current = null; + } + }; + + const commit = interactivePageLayout.current || (() => {}); + + return ( +
+
+
+
+
+ +
+ +
+ {/* NOTE: canvasWorkpadContainer is used for exporting */} +
+ +
+
+
+ + {isWriteable && ( +
+ +
+ )} +
+ +
+ +
+
+
+ ); +}; + +WorkpadApp.propTypes = { + isWriteable: PropTypes.bool.isRequired, + deselectElement: PropTypes.func, +}; diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js deleted file mode 100644 index fc3ac9922355a5..00000000000000 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Sidebar } from '../../../components/sidebar'; -import { Toolbar } from '../../../components/toolbar'; -import { Workpad } from '../../../components/workpad'; -import { WorkpadHeader } from '../../../components/workpad_header'; -import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants'; - -export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer'; - -export class WorkpadApp extends React.PureComponent { - static propTypes = { - isWriteable: PropTypes.bool.isRequired, - deselectElement: PropTypes.func, - }; - - interactivePageLayout = null; // future versions may enable editing on multiple pages => use array then - - registerLayout(newLayout) { - if (this.interactivePageLayout !== newLayout) { - this.interactivePageLayout = newLayout; - } - } - - unregisterLayout(oldLayout) { - if (this.interactivePageLayout === oldLayout) { - this.interactivePageLayout = null; - } - } - - render() { - const { isWriteable, deselectElement } = this.props; - - return ( -
-
-
-
-
- {})} /> -
- -
- {/* NOTE: canvasWorkpadContainer is used for exporting */} -
- -
-
-
- - {isWriteable && ( -
- -
- )} -
- -
- -
-
-
- ); - } -} diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts new file mode 100644 index 00000000000000..46f2efaf5e7d2b --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { MouseEventHandler } from 'react'; +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { canUserWrite } from '../../../state/selectors/app'; +import { getWorkpad, isWriteable } from '../../../state/selectors/workpad'; +import { WorkpadApp as Component } from './workpad_app.component'; +import { withElementsLoadedTelemetry } from './workpad_telemetry'; +import { State } from '../../../../types'; + +export { WORKPAD_CONTAINER_ID } from './workpad_app.component'; + +const mapDispatchToProps = (dispatch: Dispatch): { deselectElement: MouseEventHandler } => ({ + deselectElement: (ev) => { + ev.stopPropagation(); + dispatch(selectToplevelNodes([])); + }, +}); + +export const WorkpadApp = connect( + (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + workpad: getWorkpad(state), + }), + mapDispatchToProps +)(withElementsLoadedTelemetry(Component)); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index eb4b451896b46b..b1e87ca67f5e5c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -18,22 +18,25 @@ import { EditMenu } from './edit_menu'; import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; +import { CommitFn } from '../../../types'; const { WorkpadHeader: strings } = ComponentStrings; export interface Props { isWriteable: boolean; - toggleWriteable: () => void; canUserWrite: boolean; - commit: (type: string, payload: any) => any; + commit: CommitFn; + onSetWriteable?: (writeable: boolean) => void; } export const WorkpadHeader: FunctionComponent = ({ isWriteable, canUserWrite, - toggleWriteable, commit, + onSetWriteable = () => {}, }) => { + const toggleWriteable = () => onSetWriteable(!isWriteable); + const keyHandler = (action: string) => { if (action === 'EDITING') { toggleWriteable(); @@ -145,6 +148,7 @@ export const WorkpadHeader: FunctionComponent = ({ WorkpadHeader.propTypes = { isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, + commit: PropTypes.func.isRequired, + onSetWriteable: PropTypes.func, canUserWrite: PropTypes.bool, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 1f630040b0c36c..0661aa4be4313b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -10,37 +10,16 @@ import { canUserWrite } from '../../state/selectors/app'; import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; import { setWriteable } from '../../state/actions/workpad'; import { State } from '../../../types'; -import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component'; +import { WorkpadHeader as Component } from './workpad_header.component'; -interface StateProps { - isWriteable: boolean; - canUserWrite: boolean; - selectedPage: string; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; -} - -const mapStateToProps = (state: State): StateProps => ({ +const mapStateToProps = (state: State) => ({ isWriteable: isWriteable(state) && canUserWrite(state), canUserWrite: canUserWrite(state), selectedPage: getSelectedPage(state), }); const mapDispatchToProps = (dispatch: Dispatch) => ({ - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + onSetWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), }); -export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index cc07f498f1eec2..6b3f9ad3e8043d 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -76,3 +76,7 @@ export interface CanvasWorkpadBoundingBox { top: number; bottom: number; } + +export type LayoutState = any; + +export type CommitFn = (type: string, payload: any) => LayoutState; From 37ce10158ad8911bbbd951a5b5e329858beec091 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 10 Aug 2020 09:08:36 +0200 Subject: [PATCH 27/31] RFC: encryption key rotation support for the `encryptedSavedObjects` plugin (#72828) --- rfcs/text/0012_encryption_key_rotation.md | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 rfcs/text/0012_encryption_key_rotation.md diff --git a/rfcs/text/0012_encryption_key_rotation.md b/rfcs/text/0012_encryption_key_rotation.md new file mode 100644 index 00000000000000..d984d1157a0a10 --- /dev/null +++ b/rfcs/text/0012_encryption_key_rotation.md @@ -0,0 +1,119 @@ +- Start Date: 2020-07-22 +- RFC PR: [#72828](https://github.com/elastic/kibana/pull/72828) +- Kibana Issue: (leave this empty) + +# Summary + +This RFC proposes a way of the encryption key (`xpack.encryptedSavedObjects.encryptionKey`) rotation that would allow administrators to seamlessly change existing encryption key without any data loss and manual intervention. + +# Basic example + +When administrators decide to rotate encryption key they will have to generate a new one and move the old key(s) to the `keyRotation` section in the `kibana.yml`: + +```yaml +xpack.encryptedSavedObjects: + encryptionKey: "NEW-encryption-key" + keyRotation: + decryptionOnlyKeys: ["OLD-encryption-key-1", "OLD-encryption-key-2"] +``` + +Before old decryption-only key is disposed administrators may want to call a dedicated and _protected_ API endpoint that will go through all registered Saved Objects with encrypted attributes and try to re-encrypt them with the primary encryption key: + +```http request +POST https://localhost:5601/api/encrypted_saved_objects/rotate_key?conflicts=abort +Content-Type: application/json +Kbn-Xsrf: true +``` + +# Motivation + +Today when encryption key changes we can no longer decrypt Saved Objects attributes that were previously encrypted with the `EncryptedSavedObjects` plugin. We handle this case in two different ways depending on whether consumers explicitly requested decryption or not: + +* If consumers explicitly request decryption via `getDecryptedAsInternalUser()` we abort operation and throw exception. +* If consumers fetch Saved Objects with encrypted attributes that should be automatically decrypted (the ones with `dangerouslyExposeValue: true` marker) via standard Saved Objects APIs we don't abort operation, but rather strip all encrypted attributes from the response and record decryption error in the `error` Saved Object field. +* If Kibana tries to migrate encrypted Saved Objects at the start up time we abort operation and throw exception. + +In both of these cases we throw or record error with the specific type to allow consumers to gracefully handle this scenario and either drop Saved Objects with unrecoverable encrypted attributes or facilitate the process of re-entering and re-encryption of the new values. + +This approach works reasonably well in some scenarios, but it may become very troublesome if we have to deal with lots of Saved Objects. Moreover, we'd like to recommend our users to periodically rotate encryption keys even if they aren't compromised. Hence, we need to provide a way of seamless migration of the existing encrypted Saved Objects to a new encryption key. + +There are two main scenarios we'd like to cover in this RFC: + +## Encryption key is not available + +Administrators may lose existing encryption key or explicitly decide to not use it if it was compromised and users can no longer trust encrypted content that may have been tampered with. In this scenario encrypted portion of the existing Saved Objects is considered lost, and the only way to recover from this state is a manual intervention described previously. That means `EncryptedSavedObjects` plugin consumers __should__ continue supporting this scenario even after we implement a proper encryption key rotation mechanism described in this RFC. + +## Encryption key is available, but needs to be rotated + +In this scenario a new encryption key (primary encryption key) will be generated, and we will use it to encrypt new or updated Saved Objects. We will still need to know the old encryption key to decrypt existing attributes, but we will no longer use this key to encrypt any of the new or existing Saved Objects. It's also should be possible to have multiple old decryption-only keys. + +The old old decryption-only keys should be eventually disposed and users should have a way to make sure all existing Saved Objects are re-encrypted with the new primary encryption key. + +__NOTE:__ users can get into a state when different Saved Objects are encrypted with different encryption keys even if they didn't intend to rotate the encryption key. We anticipate that it can happen during initial Elastic Stack HA setup, when by mistake or intentionally different Kibana instances were using different encryption keys. Key rotation mechanism can help to fix this issue without a data loss. + +# Detailed design + +The core idea is that when the encryption key needs to be rotated then a new key is generated and becomes a primary one, and the old one moves to the `keyRotation` section: + +```yaml +xpack.encryptedSavedObjects: + encryptionKey: "NEW-encryption-key" + keyRotation: + decryptionOnlyKeys: ["OLD-encryption-key"] +``` + +As the name implies, the key from the `decryptionOnlyKeys` is only used to decrypt content that we cannot decrypt with the primary encryption key. It's allowed to have multiple decryption-only keys at the same time. When user creates a new Saved Object or updates the existing one then its content is always encrypted with the primary encryption key. Config schema won't allow having the same key in `encryptionKey` and `decryptionOnlyKeys`. + +Having multiple decryption keys at the same time brings one problem though: we need to figure out which key to use to decrypt specific Saved Object. If our encryption keys could have a unique ID that we would store together with the encrypted data (we cannot use encryption key hash for that for obvious reasons) we could know for sure which key to use, but we don't have such functionality right now and it may not be the easiest one to manage through `yml` configuration anyway. + +Instead, this RFC proposes to try available existing decryption keys one by one to decrypt Saved Object and always start from the primary one. This way we won't incur any penalty while decrypting Saved Objects that are already encrypted with the primary encryption key, but there will still be some cost when we have to perform multiple decryption attempts. See the [`Drawbacks`](#drawbacks) section for the details. + +Technically just having `decryptionOnlyKeys` would be enough to cover the majority of the use cases, but the old decryption-only keys should be eventually disposed. At this point administrators would like to make sure _all_ Saved Objects are encrypted with the new primary encryption key. Another reason to re-encrypt all existing Saved Objects with the new key at once is to preventively reduce the performance impact of the multiple decryption attempts. + +We'd like to make this process as simple as possible while meeting the following requirements: + +* It should not be required to restart Kibana to perform this type of migration since Saved Objects encrypted with the another encryption key can theoretically appear at any point in time. +* It should be possible to integrate this operation into other operational flows our users may have and any user-friendly key management UIs we may introduce in this future. +* Any possible failures that may happen during this operation shouldn't make Kibana nonfunctional. +* Ordinary users should not be able to trigger this migration since it may consume a considerable amount of computing resources. + +We think that the best option we have right now is a dedicated API endpoint that would trigger this migration: + +```http request +POST https://localhost:5601/api/encrypted_saved_objects/rotate_key?conflicts=abort +Content-Type: application/json +Kbn-Xsrf: true +``` + +This will be a protected endpoint and only user with enough privileges will be able to use it. + +Under the hood we'll scroll over all Saved Objects that are registered with `EncryptedSavedObjects` plugin and re-encrypt attributes only for those of them that can only be decrypted with any of the old decryption-only keys. Saved Objects that can be decrypted with the primary encryption key will be ignored. We'll also ignore the ones that cannot be decrypted with any of the available decryption keys at all, and presumably return their IDs in the response. + +As for any other encryption or decryption operation we'll record relevant bits in the audit logs. + +# Benefits + +* The concept of decryption-only keys is easy to grasp and allows Kibana to function even if it has a mix of Saved Objects encrypted with different encryption keys. +* Support of the key rotation out of the box decreases the chances of the data loss and makes `EncryptedSavedObjects` story more secure and approachable overall. + +# Drawbacks + +* Multiple decryption attempts affect performance. See [the performance test results](https://github.com/elastic/kibana/pull/72420#issue-453400211) for more details, but making two decryption attempts is basically twice as slow as with a single attempt. Although it's only relevant for the encrypted Saved Objects migration performed at the start up time and batch operations that trigger automatic decryption (only for the Saved Objects registered with `dangerouslyExposeValue: true` marker that nobody is using in Kibana right now), we may have more use cases in the future. +* Historically we supported Kibana features with either configuration or dedicated UI, but in this case we want to introduce an API endpoint that _should be_ used directly. We may have a key management UI in the future though. + +# Alternatives + +We cannot think of any better alternative for `decryptionOnlyKeys` at the moment, but instead of API endpoint for the batch re-encryption we could potentially use another `kibana.yml` config option. For example `keyRotation.mode: onWrite | onStart | both`, but it feels a bit hacky and cannot be really integrated with anything else. + +# Adoption strategy + +Adoption strategy is pretty straightforward since the feature is an enhancement and doesn't bring any BWC concerns. + +# How we teach this + +Key rotation is a well-known paradigm. We'll update `README.md` of the `EncryptedSavedObjects` plugin and create a dedicated section in the public Kibana documentation. + +# Unresolved questions + +* Is it reasonable to have this feature in Basic? +* Are there any other use-cases that are not covered by the proposal? From 0a65e172a1563c9b0e39d06c5a4c0a3a4af480ec Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 10 Aug 2020 09:30:51 +0200 Subject: [PATCH 28/31] [ES UI Shared] Added README (#72034) * Added readme to es-ui-shared * implement PR feedback; clarify terms and tighten grammar * added note about intended users of es ui shared modules --- src/plugins/es_ui_shared/README.md | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/plugins/es_ui_shared/README.md diff --git a/src/plugins/es_ui_shared/README.md b/src/plugins/es_ui_shared/README.md new file mode 100644 index 00000000000000..5a9091e2dd1ebd --- /dev/null +++ b/src/plugins/es_ui_shared/README.md @@ -0,0 +1,32 @@ +## ES UI shared modules + +This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. + +**Please note**: Modules in ES UI shared are intended for use by the ES UI Management Team (elastic/es-ui@) only. Please reach out to us if there is something you would like to contribute or use in these modules. + +## Files and folders overview + +- `./public` | `./server`. Folders for grouping server or public code according to the Kibana plugin pattern. +- `./__packages_do_not_import__` is where actual functionality is kept. This enables modules more control over what functionality is directly exported and prevents parts of modules to be depended on externally in unintended ways. +- `./public/index.ts` | `./server/index.ts` These files export modules (simple JavaScript objects). For example, `Monaco` is the name of a module. In this way, modules namespace all of their exports and do not have to be concerned about name collisions from other modules. + +## Conventions for adding code + +When adding new functionality, look at the folders in `./__packages_do_not_import__` and consider whether your functionality falls into any of those modules. + +If it does not, you should create a module and expose it to public or server code (or both) following the conventions described above. + +### Example + +If I wanted to add functionality for calculating a Fibonacci sequence browser-side one would do the following: + +1. Create a folder `./__packages_do_not_import__/math`. The name of the folder should be a snake_case version of the module name. In this case `Math` -> `math`. Another case, `IndexManagement` -> `index_management`. +2. Write your function in `./__packages_do_not_import__/math/calculate_fibonacci.ts`, adding any relevant tests in the same folder. +3. Export functionality intended _for consumers_ from `./__packages_do_not_import__/math/index.ts`. +4. Create a folder `./public/math`. +5. Export all functionality from `./__packages_do_not_import__/math` in `./public/math/index.ts`. +6. In `./public/index.ts` import `./public/math` using `import * as Math from './public/math;`. The name (`Math`) given here is really important and will be what consumers depend on. +7. Add the `Math` module to the list of exported modules in `./public/index.ts`, e.g. `export { <...other modules>, Math }` +8. Use `Math` in your public side code elsewhere! + +This example assumes no other appropriate home for such a function exists. From ce025732a17bb1a1488e6d01abf73545d9a393ba Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Mon, 10 Aug 2020 10:09:30 +0200 Subject: [PATCH 29/31] add retry for checking Add button (#74551) Co-authored-by: Elastic Machine --- .../test/functional/apps/dashboard/_async_dashboard.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index cc30a7a7e640fb..8851c83dea1ffa 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -27,8 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); - // Flakky: https://github.com/elastic/kibana/issues/65949 - describe.skip('sample data dashboard', function describeIndexTests() { + describe('sample data dashboard', function describeIndexTests() { before(async () => { await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -36,8 +35,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.home.addSampleDataSet('flights'); - const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); - expect(isInstalled).to.be(true); + await retry.tryForTime(10000, async () => { + const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); + expect(isInstalled).to.be(true); + }); + // add the range of the sample data so we can pick it in the quick pick list const SAMPLE_DATA_RANGE = `[ { From ad8502c8d9cbf35822ed187aae9ea31e5eca21ab Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 10 Aug 2020 01:25:29 -0700 Subject: [PATCH 30/31] update code-exploration docs --- docs/developer/architecture/code-exploration.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index bb7222020180ca..d9502e4cb47ee4 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -86,9 +86,9 @@ Contains the Discover application and the saved search embeddable. Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. -- {kib-repo}blob/{branch}/src/plugins/es_ui_shared[esUiShared] +- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] -WARNING: Missing README. +This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. - {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] From f7f2988aa2d6a557ee2aa6aa184ea774bb57f345 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 10 Aug 2020 13:04:57 +0200 Subject: [PATCH 31/31] [ML] Functional tests - stabilize DFA job type check (#74631) This PR stabilizes the data frame analytics job type assertion by adding a retry. --- .../ml/data_frame_analytics_creation.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index a49febfe68f614..d8df2fb869ed7f 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -46,14 +46,16 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async assertJobTypeSelection(expectedSelection: string) { - const actualSelection = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobWizardJobTypeSelect', - 'value' - ); - expect(actualSelection).to.eql( - expectedSelection, - `Job type selection should be '${expectedSelection}' (got '${actualSelection}')` - ); + await retry.tryForTime(5000, async () => { + const actualSelection = await testSubjects.getAttribute( + 'mlAnalyticsCreateJobWizardJobTypeSelect', + 'value' + ); + expect(actualSelection).to.eql( + expectedSelection, + `Job type selection should be '${expectedSelection}' (got '${actualSelection}')` + ); + }); }, async selectJobType(jobType: string) {