From 535aff77bf376300a0570bd3537c2e79e3a90c67 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 3 Jul 2020 09:03:03 -0700 Subject: [PATCH 01/12] [Ingest Manager] Add ability to sort to agent configs and package configs (#70676) (#70686) * Add sorting params to list endpoints; allow sorting on agent config and package config tables; normalize casing of 'desc' and 'asc' * Fix es archiver data * Fix tests --- .../common/types/rest_spec/common.ts | 6 ++-- .../ingest_manager/hooks/index.ts | 1 + .../ingest_manager/hooks/use_sorting.tsx | 16 +++++++++ .../step_select_config.tsx | 7 +++- .../package_configs/package_configs_table.tsx | 2 ++ .../sections/agent_config/list_page/index.tsx | 32 ++++++++++++----- .../agent_reassign_config_flyout/index.tsx | 5 ++- .../server/saved_objects/index.ts | 3 +- .../server/services/agent_config.ts | 5 +-- .../server/services/agents/crud.ts | 36 +++++++------------ .../server/services/agents/events.ts | 2 +- .../server/services/agents/status.ts | 2 +- .../services/api_keys/enrollment_api_key.ts | 2 +- .../server/services/package_config.ts | 4 ++- .../server/types/rest_spec/common.ts | 6 ++-- .../es_archives/fleet/agents/data.json | 3 +- 16 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts index c52471ccfb4f5..0d1f72afa16f1 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts @@ -5,7 +5,9 @@ */ export interface ListWithKuery { - page: number; - perPage: number; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: 'desc' | 'asc'; kuery?: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 6ebfd3f28fd9b..36b7d412bf276 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -13,6 +13,7 @@ export { useLink } from './use_link'; export { useKibanaLink } from './use_kibana_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; +export { useSorting } from './use_sorting'; export { useDebounce } from './use_debounce'; export * from './use_request'; export * from './use_input'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx new file mode 100644 index 0000000000000..b00809249897b --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState } from 'react'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; + +export function useSorting(defaultSorting: CriteriaWithPagination['sort']) { + const [sorting, setSorting] = useState['sort']>(defaultSorting); + + return { + sorting, + setSorting, + }; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 70668c2856f98..849d7bfc63f34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -31,7 +31,12 @@ export const StepSelectConfig: React.FunctionComponent<{ data: agentConfigsData, error: agentConfigsError, isLoading: isAgentConfigsLoading, - } = useGetAgentConfigs(); + } = useGetAgentConfigs({ + page: 1, + perPage: 1000, + sortField: 'name', + sortOrder: 'asc', + }); const agentConfigs = agentConfigsData?.items || []; const agentConfigsById = agentConfigs.reduce( (acc: { [key: string]: GetAgentConfigsResponseItem }, config) => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx index 19243084f6821..42d1075e2ee1f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx @@ -118,6 +118,7 @@ export const PackageConfigsTable: React.FunctionComponent = ({ (): EuiInMemoryTableProps['columns'] => [ { field: 'name', + sortable: true, name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle', { @@ -137,6 +138,7 @@ export const PackageConfigsTable: React.FunctionComponent = ({ }, { field: 'packageTitle', + sortable: true, name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle', { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 0a9daf0038aab..4e79bd4fa7997 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -17,6 +17,7 @@ import { EuiTableFieldDataColumnType, EuiTextColor, } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; @@ -27,6 +28,7 @@ import { useCapabilities, useGetAgentConfigs, usePagination, + useSorting, useLink, useConfig, useUrlParams, @@ -84,6 +86,10 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); + const { sorting, setSorting } = useSorting({ + field: 'updated_at', + direction: 'desc', + }); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -106,6 +112,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { const { isLoading, data: agentConfigData, sendRequest } = useGetAgentConfigs({ page: pagination.currentPage, perPage: pagination.pageSize, + sortField: sorting?.field, + sortOrder: sorting?.direction, kuery: search, }); @@ -116,6 +124,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { > = [ { field: 'name', + sortable: true, name: i18n.translate('xpack.ingestManager.agentConfigList.nameColumnTitle', { defaultMessage: 'Name', }), @@ -158,6 +167,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { }, { field: 'updated_at', + sortable: true, name: i18n.translate('xpack.ingestManager.agentConfigList.updatedOnColumnTitle', { defaultMessage: 'Last updated on', }), @@ -240,6 +250,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { [createAgentConfigButton] ); + const onTableChange = (criteria: CriteriaWithPagination) => { + const newPagination = { + ...pagination, + currentPage: criteria.page.index + 1, + pageSize: criteria.page.size, + }; + setPagination(newPagination); + setSorting(criteria.sort); + }; + return ( {isCreateAgentConfigFlyoutOpen ? ( @@ -276,7 +296,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { - loading={isLoading} hasActions={true} noItemsMessage={ @@ -314,14 +334,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { totalItemCount: agentConfigData ? agentConfigData.total : 0, pageSizeOptions, }} - onChange={({ page }: { page: { index: number; size: number } }) => { - const newPagination = { - ...pagination, - currentPage: page.index + 1, - pageSize: page.size, - }; - setPagination(newPagination); - }} + sorting={{ sort: sorting }} + onChange={onTableChange} /> ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx index 450def54ba1d0..592ca7f7b8380 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -36,7 +36,10 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl agent.config_id ); - const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigsRequest = useGetAgentConfigs({ + page: 1, + perPage: 1000, + }); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const [isSubmitting, setIsSubmitting] = useState(false); 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 98de9ac217af9..9819a4fa5d750 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -119,8 +119,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { }, mappings: { properties: { - id: { type: 'keyword' }, - name: { type: 'text' }, + name: { type: 'keyword' }, description: { type: 'text' }, namespace: { type: 'keyword' }, is_default: { type: 'boolean' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index ada35d1825069..bd00727714c33 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -143,10 +143,12 @@ class AgentConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const agentConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... @@ -273,7 +275,6 @@ class AgentConfigService { soClient, id, { - ...oldAgentConfig, package_configs: uniq( [...((oldAgentConfig.package_configs || []) as string[])].filter( (pkgConfigId) => !packageConfigIds.includes(pkgConfigId) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index c78a9ff8bb7b5..4420135aec952 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -12,20 +12,24 @@ import { AGENT_TYPE_EPHEMERAL, AGENT_POLLING_THRESHOLD_MS, } from '../../constants'; -import { AgentSOAttributes, Agent, AgentEventSOAttributes } from '../../types'; +import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; import { savedObjectToAgent } from './saved_objects'; import { escapeSearchQueryPhrase } from '../saved_object'; export async function listAgents( soClient: SavedObjectsClientContract, - options: { - page: number; - perPage: number; - kuery?: string; + options: ListWithKuery & { showInactive: boolean; } ) { - const { page, perPage, kuery, showInactive = false } = options; + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + } = options; const filters = []; @@ -49,10 +53,11 @@ export async function listAgents( const { saved_objects, total } = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, filter: _joinFilters(filters), - ..._getSortFields(), }); const agents: Agent[] = saved_objects.map(savedObjectToAgent); @@ -137,23 +142,6 @@ export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: }); } -function _getSortFields(sortOption?: string) { - switch (sortOption) { - case 'ASC': - return { - sortField: 'enrolled_at', - sortOrder: 'ASC', - }; - - case 'DESC': - default: - return { - sortField: 'enrolled_at', - sortOrder: 'DESC', - }; - } -} - function _joinFilters(filters: string[], operator = 'AND') { return filters.reduce((acc: string | undefined, filter) => { if (acc) { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index b6d87c9ca5b2f..55970607c74ab 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -31,7 +31,7 @@ export async function getAgentEvents( perPage, page, sortField: 'timestamp', - sortOrder: 'DESC', + sortOrder: 'desc', defaultSearchOperator: 'AND', search: agentId, searchFields: ['agent_id'], diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 0efb202eff532..016a2344cf532 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -61,7 +61,7 @@ async function getEventsCount(soClient: SavedObjectsClientContract, configId?: s perPage: 0, page: 1, sortField: 'timestamp', - sortOrder: 'DESC', + sortOrder: 'desc', defaultSearchOperator: 'AND', }); diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index 3b003f47eb6f9..02e2c8151fac7 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -29,7 +29,7 @@ export async function listEnrollmentApiKeys( page, perPage, sortField: 'created_at', - sortOrder: 'DESC', + sortOrder: 'desc', filter: kuery && kuery !== '' ? kuery.replace( 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 c886f4868ad30..5a7546bfee2e0 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -145,10 +145,12 @@ class PackageConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const packageConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts index 2c8134d2e8f92..dc0f111680490 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts @@ -6,8 +6,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ListWithKuerySchema = schema.object({ - page: schema.number({ defaultValue: 1 }), - perPage: schema.number({ defaultValue: 20 }), + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), kuery: schema.maybe(schema.string()), }); diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index c317aad8ba05b..b3d49199b0d9e 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -220,8 +220,7 @@ ], "revision": 2, "updated_at": "2020-05-07T19:34:42.533Z", - "updated_by": "system", - "id": "config1" + "updated_by": "system" } } } From 654ad964fe0793e04e63bdffc085cf15e1e6e7ab Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Fri, 3 Jul 2020 12:24:15 -0400 Subject: [PATCH 02/12] [Alerting] document requirements for developing new action types (#69164) (#70684) * [Alerting] document requirements for developing new action types resolves https://github.com/elastic/kibana/issues/67864 Adding this to the actions README.md, it should be published as asciidocs at GA. --- x-pack/plugins/actions/README.md | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 605676cee363d..494f2f38e8bff 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -71,6 +71,7 @@ Table of Contents - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) +- [Developing New Action Types](#developing-new-action-types) ## Terminology @@ -606,3 +607,39 @@ $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack. "version": "WzMsMV0=" } ``` + +# Developing New Action Types + +When creating a new action type, your plugin will eventually call `server.plugins.actions.setup.registerType()` to register the type with the actions plugin, but there are some additional things to think about about and implement. + +Consider working with the alerting team on early structure /design feedback of new actions, especially as the APIs and infrastructure are still under development. + +## licensing + +Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. + +## plugin location + +Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. + +Actions that take URLs or hostnames should check that those values are whitelisted. The whitelisting utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). + +## documentation + +You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). + +## tests + +The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) + +## action type config and secrets + +Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. + +This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. + +Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. + +## user interface + +In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). From 2c0651e6d885167d66c08569132a79c27fbd608e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 3 Jul 2020 18:37:16 +0200 Subject: [PATCH 03/12] [Lens] Add ability to set colors for y-axis series (#70311) (#70719) --- .../__snapshots__/xy_expression.test.tsx.snap | 14 ++ .../public/xy_visualization/state_helpers.ts | 11 +- .../public/xy_visualization/to_expression.ts | 3 +- .../lens/public/xy_visualization/types.ts | 5 + .../xy_visualization/xy_config_panel.tsx | 230 +++++++++++++----- .../xy_visualization/xy_expression.test.tsx | 69 ++++++ .../public/xy_visualization/xy_expression.tsx | 3 +- 7 files changed, 275 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index 8cb30037379da..6e87e47a5cf9f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -36,6 +36,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` title="a" /> { + if (layer.splitAccessor) { + return null; + } + return ( + layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 6ec22270d8b18..b5b796dc019de 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -188,7 +188,8 @@ export const buildExpression = ( function: 'lens_xy_yConfig', arguments: { forAccessor: [yConfig.forAccessor], - axisMode: [yConfig.axisMode], + axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], + color: yConfig.color ? [yConfig.color] : [], }, }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index e62c5f60a58e1..8ea9683ca042c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -100,6 +100,10 @@ export const yAxisConfig: ExpressionFunctionDefinition< options: ['auto', 'left', 'right'], help: 'The axis mode of the metric', }, + color: { + types: ['string'], + help: 'The color of the series', + }, }, fn: function fn(input: unknown, args: YConfig) { return { @@ -195,6 +199,7 @@ export type YAxisMode = 'auto' | 'left' | 'right'; export interface YConfig { forAccessor: string; axisMode?: YAxisMode; + color?: string; } export interface LayerConfig { 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 3e73cd256bdbf..e6c284f09ab4e 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 @@ -4,12 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; +import { debounce } from 'lodash'; +import { + EuiButtonGroup, + EuiFormRow, + htmlIdGenerator, + EuiForm, + EuiColorPicker, + EuiColorPickerProps, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; import { State, SeriesType, visualizationTypes, YAxisMode } from './types'; import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types'; -import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; +import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; type UnwrapArray = T extends Array ? P : T; @@ -70,70 +80,176 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const idPrefix = htmlIdGenerator()(); -export function DimensionEditor({ - state, - setState, - layerId, - accessor, -}: VisualizationDimensionEditorProps) { +export function DimensionEditor(props: VisualizationDimensionEditorProps) { + const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; const axisMode = (layer.yConfig && layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) || 'auto'; + return ( - - + + + { - const newMode = id.replace(idPrefix, '') as YAxisMode; - const newYAxisConfigs = [...(layer.yConfig || [])]; - const existingIndex = newYAxisConfigs.findIndex( - (yAxisConfig) => yAxisConfig.forAccessor === accessor - ); - if (existingIndex !== -1) { - newYAxisConfigs[existingIndex].axisMode = newMode; + > + { + const newMode = id.replace(idPrefix, '') as YAxisMode; + const newYAxisConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYAxisConfigs.findIndex( + (yAxisConfig) => yAxisConfig.forAccessor === accessor + ); + if (existingIndex !== -1) { + newYAxisConfigs[existingIndex].axisMode = newMode; + } else { + newYAxisConfigs.push({ + forAccessor: accessor, + axisMode: newMode, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index)); + }} + /> + + + ); +} + +const tooltipContent = { + auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { + defaultMessage: 'Lens automatically picks colors for you unless you specify a custom color.', + }), + custom: i18n.translate('xpack.lens.configPanel.color.tooltip.custom', { + defaultMessage: 'Clear the custom color to return to “Auto” mode.', + }), + disabled: i18n.translate('xpack.lens.configPanel.color.tooltip.disabled', { + defaultMessage: + 'Individual series cannot be custom colored when the layer includes a “Break down by“', + }), +}; + +const ColorPicker = ({ + state, + setState, + layerId, + accessor, +}: VisualizationDimensionEditorProps) => { + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + const disabled = !!layer.splitAccessor; + + const [color, setColor] = useState(getSeriesColor(layer, accessor)); + + const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { + setColor(text); + if (output.isValid || text === '') { + updateColorInState(text, output); + } + }; + + const updateColorInState: EuiColorPickerProps['onChange'] = React.useMemo( + () => + debounce((text, output) => { + const newYConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor); + if (existingIndex !== -1) { + if (text === '') { + delete newYConfigs[existingIndex].color; } else { - newYAxisConfigs.push({ - forAccessor: accessor, - axisMode: newMode, - }); + newYConfigs[existingIndex].color = output.hex; } - setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index)); - }} - /> + } else { + newYConfigs.push({ + forAccessor: accessor, + color: output.hex, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index)); + }, 256), + [state, layer, accessor, index] + ); + + return ( + + + {i18n.translate('xpack.lens.xyChart.seriesColor.label', { + defaultMessage: 'Series color', + })}{' '} + + + + } + > + {disabled ? ( + + + + ) : ( + + )} ); -} +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index f433a88e3bdbd..4a532d378eaf7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -996,6 +996,75 @@ describe('xy_expression', () => { }); }); + describe('y series coloring', () => { + test('color is applied to chart for multiple series', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['a', 'b'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + { + forAccessor: 'b', + color: '#FFFF00', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + yConfig: [ + { + forAccessor: 'c', + color: '#FEECDF', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual('#550000'); + expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual('#FFFF00'); + expect((component.find(LineSeries).at(2).prop('color') as Function)!()).toEqual('#FEECDF'); + }); + test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual(null); + expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual(null); + }); + }); + describe('provides correct series naming', () => { const nameFnArgs = { seriesKeys: [], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 3ff7bd7fda304..3e5fb10e080d0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -35,7 +35,7 @@ import { } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; -import { isHorizontalChart } from './state_helpers'; +import { isHorizontalChart, getSeriesColor } from './state_helpers'; import { parseInterval } from '../../../../../src/plugins/data/common'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { EmptyPlaceholder } from '../shared_components'; @@ -433,6 +433,7 @@ export function XYChart({ data: rows, xScaleType, yScaleType, + color: () => getSeriesColor(layer, accessor), groupId: yAxesConfiguration.find((axisConfiguration) => axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) )?.groupId, From cc1b8565719f54520137d8646d4fe54412ad31de Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 3 Jul 2020 21:43:21 +0200 Subject: [PATCH 04/12] [7.x] [RUM Dashboard] Added service name filter (#70349) (#70721) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- x-pack/plugins/apm/common/agent_name.ts | 6 +- .../app/RumDashboard/ClientMetrics/index.tsx | 6 +- .../PageLoadDistribution/index.tsx | 13 +++- .../PageLoadDistribution/use_breakdowns.ts | 6 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 6 +- .../components/app/RumDashboard/index.tsx | 43 ++++++++++- .../ServiceNameFilter/index.tsx | 74 +++++++++++++++++++ .../lib/ui_filters/local_ui_filters/config.ts | 7 ++ 8 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 630f5739806af..9d462dad87ec0 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -30,10 +30,12 @@ export function isAgentName(agentName: string): agentName is AgentName { return AGENT_NAMES.includes(agentName as AgentName); } +export const RUM_AGENTS = ['js-base', 'rum-js']; + export function isRumAgentName( - agentName: string | undefined + agentName?: string ): agentName is 'js-base' | 'rum-js' { - return agentName === 'js-base' || agentName === 'rum-js'; + return RUM_AGENTS.includes(agentName!); } export function isJavaAgentName( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 776f74a169966..df72fa604e4b3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { @@ -35,7 +35,7 @@ export function ClientMetrics() { }); } }, - [start, end, uiFilters] + [start, end, serviceName, uiFilters] ); const STAT_STYLE = { width: '240px' }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index c6b34c8b76698..7d48cee49b104 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -27,7 +27,7 @@ export interface PercentileRange { export const PageLoadDistribution = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -38,7 +38,7 @@ export const PageLoadDistribution = () => { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution', params: { @@ -57,7 +57,14 @@ export const PageLoadDistribution = () => { }); } }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + serviceName, + uiFilters, + percentileRange.min, + percentileRange.max, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c9569..805d19e2321d5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,13 +17,13 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; return useFetcher( (callApmApi) => { - if (start && end && field && value) { + if (start && end && serviceName && field && value) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution/breakdown', params: { @@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, serviceName, uiFilters, field, value, minP, maxP] ); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 34347f3f95947..328b873ef8562 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export const PageViewsTrend = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [breakdowns, setBreakdowns] = useState([]); const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-view-trends', params: { @@ -40,7 +40,7 @@ export const PageViewsTrend = () => { }); } }, - [end, start, uiFilters, breakdowns] + [end, start, serviceName, uiFilters, breakdowns] ); const onBreakdownChange = (values: BreakdownItem[]) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8f21065b0dab0..c9e475ef15316 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; import { RumDashboard } from './RumDashboard'; +import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { RUM_AGENTS } from '../../../../common/agent_name'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -24,12 +33,42 @@ export function RumOverview() { return config; }, []); + const { + urlParams: { start, end }, + } = useUrlParams(); + + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/services', + params: { + query: { + start, + end, + uiFilters: JSON.stringify({ agentName: RUM_AGENTS }), + }, + }, + }); + } + }, + [start, end] + ); + return ( <> - + + service.serviceName) ?? [] + } + /> + + + diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx new file mode 100644 index 0000000000000..e12a4a4831e17 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -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 React, { useEffect } from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + serviceNames: string[]; +} + +const ServiceNameFilter = ({ serviceNames }: Props) => { + const { + urlParams: { serviceName }, + } = useUrlParams(); + + const options = serviceNames.map((type) => ({ + text: type, + value: type, + })); + + const updateServiceName = (serviceN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + serviceName: serviceN, + }), + }; + history.push(newLocation); + }; + + useEffect(() => { + if (!serviceName && serviceNames.length > 0) { + updateServiceName(serviceNames[0]); + } + }, [serviceNames, serviceName]); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + })} +

+
+ + + + { + updateServiceName(event.target.value); + }} + /> + + ); +}; + +export { ServiceNameFilter }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 7a3d9d94dec8e..9f2483ab8a24e 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -16,6 +16,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_OS, CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; const filtersByName = { @@ -85,6 +86,12 @@ const filtersByName = { }), fieldName: USER_AGENT_OS, }, + serviceName: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + }), + fieldName: SERVICE_NAME, + }, }; export type LocalUIFilterName = keyof typeof filtersByName; From dd248633f61d9263355a90ebb74b2f6e3f7db01d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 3 Jul 2020 21:43:48 +0200 Subject: [PATCH 05/12] [7.x] [RUM Dashboard] Update rum title to be consistent with APM (#70460) (#70720) --- .../apm/public/components/app/Home/index.tsx | 7 +- .../app/RumDashboard/RumDashboard.tsx | 67 +++++++------------ .../app/RumDashboard/translations.ts | 8 --- 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 69699b72a96df..f612ac0d383ef 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -27,7 +27,6 @@ import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; import { RumOverview } from '../RumDashboard'; import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; -import { I18LABELS } from '../RumDashboard/translations'; function getHomeTabs({ serviceMapEnabled = true, @@ -109,11 +108,7 @@ export function Home({ tab }: Props) { -

- {selectedTab.name === 'rum-overview' - ? I18LABELS.endUserExperience - : 'APM'} -

+

APM

diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index cd50f3b575113..326d4a00fd31f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -16,50 +16,33 @@ import { ClientMetrics } from './ClientMetrics'; import { PageViewsTrend } from './PageViewsTrend'; import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; export function RumDashboard() { - const { urlParams } = useUrlParams(); - - const { environment } = urlParams; - - let environmentLabel = environment || 'all environments'; - - if (environment === 'ENVIRONMENT_NOT_DEFINED') { - environmentLabel = 'undefined environment'; - } - return ( - <> - -

{I18LABELS.getWhatIsGoingOn(environmentLabel)}

-
- - - - - - - -

{I18LABELS.pageLoadTimes}

-
- - -
-
-
-
- - - - - - - - - - -
- + + + + + + +

{I18LABELS.pageLoadTimes}

+
+ + +
+
+
+
+ + + + + + + + + + +
); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 4da7b59ec7fa5..2784d9bfd8efa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -7,14 +7,6 @@ import { i18n } from '@kbn/i18n'; export const I18LABELS = { - endUserExperience: i18n.translate('xpack.apm.rum.dashboard.title', { - defaultMessage: 'End User Experience', - }), - getWhatIsGoingOn: (environmentVal: string) => - i18n.translate('xpack.apm.rum.dashboard.environment.title', { - defaultMessage: `What's going on in {environmentVal}?`, - values: { environmentVal }, - }), backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', { defaultMessage: 'Backend', }), From 4cc8ca8a78c7bce57610db0747806d43204df231 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 3 Jul 2020 21:44:47 +0200 Subject: [PATCH 06/12] [7.x] [Uptime] Use elastic charts donut (#70364) (#70722) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../__snapshots__/chart_wrapper.test.tsx.snap | 2 - .../__snapshots__/donut_chart.test.tsx.snap | 412 +++++++++++++++++- .../charts/__tests__/chart_wrapper.test.tsx | 9 +- .../components/common/charts/donut_chart.tsx | 97 ++--- .../common/charts/donut_chart_legend.tsx | 2 +- .../__snapshots__/snapshot.test.tsx.snap | 1 - .../components/overview/snapshot/snapshot.tsx | 8 +- 7 files changed, 442 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap index 71690432fd01b..d8235765bda2d 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -123,7 +123,6 @@ exports[`ChartWrapper component renders the component with loading false 1`] = ` down={8} height={144} up={4} - width={144} /> @@ -252,7 +251,6 @@ exports[`ChartWrapper component renders the component with loading true 1`] = ` down={8} height={144} up={4} - width={144} /> - + baseTheme={ + Object { + "arcSeriesStyle": Object { + "arc": Object { + "opacity": 1, + "stroke": "black", + "strokeWidth": 1, + "visible": true, + }, + }, + "areaSeriesStyle": Object { + "area": Object { + "opacity": 0.3, + "visible": true, + }, + "line": Object { + "opacity": 1, + "strokeWidth": 1, + "visible": true, + }, + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": false, + }, + }, + "axes": Object { + "axisLineStyle": Object { + "stroke": "#eaeaea", + "strokeWidth": 1, + }, + "axisTitleStyle": Object { + "fill": "#333", + "fontFamily": "sans-serif", + "fontSize": 12, + "fontStyle": "bold", + "padding": 8, + }, + "gridLineStyle": Object { + "horizontal": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "#D3DAE6", + "strokeWidth": 1, + "visible": true, + }, + "vertical": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "#D3DAE6", + "strokeWidth": 1, + "visible": true, + }, + }, + "tickLabelStyle": Object { + "fill": "#777", + "fontFamily": "sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "padding": 4, + }, + "tickLineStyle": Object { + "stroke": "#eaeaea", + "strokeWidth": 1, + "visible": true, + }, + }, + "background": Object { + "color": "transparent", + }, + "barSeriesStyle": Object { + "displayValue": Object { + "fill": "#777", + "fontFamily": "sans-serif", + "fontSize": 8, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": Object { + "opacity": 1, + }, + "rectBorder": Object { + "strokeWidth": 0, + "visible": false, + }, + }, + "bubbleSeriesStyle": Object { + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": true, + }, + }, + "chartMargins": Object { + "bottom": 10, + "left": 10, + "right": 10, + "top": 10, + }, + "chartPaddings": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "colors": Object { + "defaultVizColor": "red", + "vizColors": Array [ + "#1EA593", + "#2B70F7", + "#CE0060", + "#38007E", + "#FCA5D3", + "#F37020", + "#E49E29", + "#B0916F", + "#7B000B", + "#34130C", + ], + }, + "crosshair": Object { + "band": Object { + "fill": "#F5F5F5", + "visible": true, + }, + "line": Object { + "dash": Array [ + 5, + 5, + ], + "stroke": "#777", + "strokeWidth": 1, + "visible": true, + }, + }, + "legend": Object { + "horizontalHeight": 64, + "spacingBuffer": 10, + "verticalWidth": 200, + }, + "lineSeriesStyle": Object { + "line": Object { + "opacity": 1, + "strokeWidth": 1, + "visible": true, + }, + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": true, + }, + }, + "scales": Object { + "barsPadding": 0.25, + "histogramPadding": 0.05, + }, + "sharedStyle": Object { + "default": Object { + "opacity": 1, + }, + "highlighted": Object { + "opacity": 1, + }, + "unhighlighted": Object { + "opacity": 0.25, + }, + }, + } + } + renderer="canvas" + size={125} + theme={ + Object { + "areaSeriesStyle": Object { + "area": Object { + "opacity": 0.3, + }, + "line": Object { + "strokeWidth": 2, + }, + "point": Object { + "fill": "rgba(255, 255, 255, 1)", + "radius": 3, + "strokeWidth": 2, + "visible": false, + }, + }, + "axes": Object { + "axisLineStyle": Object { + "stroke": "rgba(238, 240, 243, 1)", + }, + "axisTitleStyle": Object { + "fill": "rgba(52, 55, 65, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 12, + "padding": 10, + }, + "gridLineStyle": Object { + "horizontal": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": true, + }, + "vertical": Object { + "dash": Array [ + 4, + 4, + ], + "opacity": 1, + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": true, + }, + }, + "tickLabelStyle": Object { + "fill": "rgba(105, 112, 125, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 10, + "padding": 8, + }, + "tickLineStyle": Object { + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": false, + }, + }, + "barSeriesStyle": Object { + "displayValue": Object { + "fill": "rgba(105, 112, 125, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 8, + }, + }, + "chartMargins": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "colors": Object { + "defaultVizColor": "#6092C0", + "vizColors": Array [ + "#54B399", + "#6092C0", + "#9170B8", + "#CA8EAE", + "#D36086", + "#E7664C", + "#AA6556", + "#DA8B45", + "#B9A888", + "#D6BF57", + ], + }, + "crosshair": Object { + "band": Object { + "fill": "rgba(245, 247, 250, 1)", + }, + "line": Object { + "dash": Array [ + 4, + 4, + ], + "stroke": "rgba(105, 112, 125, 1)", + "strokeWidth": 1, + }, + }, + "lineSeriesStyle": Object { + "line": Object { + "strokeWidth": 2, + }, + "point": Object { + "fill": "rgba(255, 255, 255, 1)", + "radius": 3, + "strokeWidth": 2, + }, + }, + "scales": Object { + "barsPadding": 0.25, + "histogramPadding": 0.05, + }, + } + } + > + + +
- +
+
+
+
+
+
- +
+
+
+
+
+
{ it('renders the component with loading false', () => { @@ -20,7 +19,7 @@ describe('ChartWrapper component', () => { - + ); expect(component).toMatchSnapshot(); @@ -31,7 +30,7 @@ describe('ChartWrapper component', () => { - + ); expect(component).toMatchSnapshot(); @@ -42,7 +41,7 @@ describe('ChartWrapper component', () => { - + ); @@ -64,7 +63,7 @@ describe('ChartWrapper component', () => { - + ); diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx index b4d6864423dfc..19716f0d3b1c9 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx @@ -5,10 +5,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import React, { useContext, useEffect, useRef } from 'react'; -import * as d3 from 'd3'; +import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { Chart, Datum, Partition, Settings, PartitionLayout } from '@elastic/charts'; import { DonutChartLegend } from './donut_chart_legend'; import { UptimeThemeContext } from '../../../contexts'; @@ -16,7 +16,6 @@ interface DonutChartProps { down: number; height: number; up: number; - width: number; } export const GreenCheckIcon = styled(EuiIcon)` @@ -28,72 +27,56 @@ export const GreenCheckIcon = styled(EuiIcon)` position: absolute; `; -export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { - const chartElement = useRef(null); - +export const DonutChart = ({ height, down, up }: DonutChartProps) => { const { colors: { danger, gray }, + chartTheme, } = useContext(UptimeThemeContext); - let upCount = up; - if (up === 0 && down === 0) { - upCount = 1; - } - useEffect(() => { - if (chartElement.current !== null) { - // we must remove any existing paths before painting - d3.select(chartElement.current).selectAll('g').remove(); - - const svgElement = d3 - .select(chartElement.current) - .append('g') - .attr('transform', `translate(${width / 2}, ${height / 2})`); - - const color = d3.scale.ordinal().domain(['up', 'down']).range([gray, danger]); - - const pieGenerator = d3.layout - .pie() - .value(({ value }: any) => value) - // these start/end angles will reverse the direction of the pie, - // which matches our design - .startAngle(2 * Math.PI) - .endAngle(0); - - svgElement - .selectAll('g') - .data( - // @ts-ignore pie generator expects param of type number[], but only works with - // output of d3.entries, which is like Array<{ key: string, value: number }> - pieGenerator(d3.entries({ up: upCount, down })) - ) - .enter() - .append('path') - .attr( - 'd', - // @ts-ignore attr does not expect a param of type Arc but it behaves as desired - d3.svg - .arc() - .innerRadius(width * 0.28) - .outerRadius(Math.min(width, height) / 2 - 10) - ) - .attr('fill', (d: any) => color(d.data.key) as any); - } - }, [danger, down, gray, height, upCount, width]); - return ( - - {/* When all monitors are up we show green check icon in middle of donut to indicate, all is well */} + {...chartTheme} + > + + d.value as number} + layers={[ + { + groupByRollup: (d: Datum) => d.label, + nodeLabel: (d: Datum) => d, + shape: { + fillColor: (d: Datum) => { + return d.dataName === 'Down' ? danger : gray; + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maximumSection: Infinity, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + idealFontSizeJump: 1.1, + outerSizeRatio: 0.9, + emptySizeRatio: 0.4, + circlePadding: 4, + }} + /> + {down === 0 && } diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx index a514013eeed98..cbbffdff745f8 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx @@ -12,7 +12,7 @@ import { DonutChartLegendRow } from './donut_chart_legend_row'; import { UptimeThemeContext } from '../../../contexts'; const LegendContainer = styled.div` - max-width: 260px; + max-width: 150px; min-width: 100px; @media (max-width: 767px) { min-width: 0px; diff --git a/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap index db41dfb0b04c4..2135fc32c2b5b 100644 --- a/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap @@ -14,7 +14,6 @@ exports[`Snapshot component renders without errors 1`] = ` down={2} height={144} up={8} - width={144} /> `; diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx index 8d6933ad18ced..bc54f14e87822 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx @@ -11,7 +11,6 @@ import { ChartWrapper } from '../../common/charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; -const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; interface SnapshotComponentProps { @@ -29,11 +28,6 @@ export const SnapshotComponent: React.FC = ({ count, hei - + ); From 4d1d79a0dd0c8fc27560913cb831563251ddf3f1 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 3 Jul 2020 15:49:44 -0400 Subject: [PATCH 07/12] [SECURITY] Bug fix for topN on draggables (#70450) (#70737) * back to normal * add unit test * hover issue + indexToAdd issue * fix unit test * review II * fix bug + review * simplification * do not update state when component is unmounted * fix hover action on field name Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../alerts_histogram_panel/index.tsx | 7 +- .../common/components/charts/barchart.tsx | 5 +- .../charts/draggable_legend_item.tsx | 4 +- .../drag_drop_context_wrapper.tsx | 12 +-- .../drag_and_drop/draggable_wrapper.tsx | 58 +++++++++--- .../draggable_wrapper_hover_content.test.tsx | 34 +++++++ .../draggable_wrapper_hover_content.tsx | 91 ++++++++++++++++--- .../common/components/draggables/index.tsx | 4 +- .../components/event_details/columns.tsx | 1 - .../components/matrix_histogram/index.tsx | 3 + .../components/matrix_histogram/types.ts | 5 +- .../common/components/top_n/index.test.tsx | 25 +++-- .../public/common/components/top_n/index.tsx | 77 ++++++++-------- .../public/common/components/top_n/top_n.tsx | 10 +- .../components/with_hover_actions/index.tsx | 26 +++--- .../public/common/containers/source/index.tsx | 49 ++++------ .../public/common/lib/keury/index.test.ts | 18 ++-- .../public/common/lib/keury/index.ts | 7 +- .../components/events_by_dataset/index.tsx | 3 + .../components/signals_by_category/index.tsx | 3 + .../components/fields_browser/field_items.tsx | 1 - .../fields_browser/field_name.test.tsx | 4 +- .../components/fields_browser/field_name.tsx | 41 +++++++-- .../components/manage_timeline/index.tsx | 47 +++++++--- .../components/timeline/body/index.test.tsx | 15 +++ .../components/timeline/body/index.tsx | 1 + .../timeline/body/stateful_body.tsx | 4 +- .../timelines/components/timeline/styles.tsx | 5 +- .../components/timeline/timeline.tsx | 11 ++- 29 files changed, 399 insertions(+), 172 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx index b6db6eb93d77f..b002700d7eff0 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx @@ -69,6 +69,7 @@ interface AlertsHistogramPanelProps { showLinkToAlerts?: boolean; showTotalAlertsCount?: boolean; stackByOptions?: AlertsHistogramOption[]; + timelineId?: string; title?: string; to: number; updateDateRange: UpdateDateRange; @@ -98,8 +99,9 @@ export const AlertsHistogramPanel = memo( showLinkToAlerts = false, showTotalAlertsCount = false, stackByOptions, - to, + timelineId, title = i18n.HISTOGRAM_HEADER, + to, updateDateRange, }) => { // create a unique, but stable (across re-renders) query id @@ -163,11 +165,12 @@ export const AlertsHistogramPanel = memo( `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` ), field: selectedStackByOption.value, + timelineId, value: bucket.key, })) : NO_LEGEND_DATA, // eslint-disable-next-line react-hooks/exhaustive-deps - [alertsData, selectedStackByOption.value] + [alertsData, selectedStackByOption.value, timelineId] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index 42fc2ac9b8453..fba8c3faa9237 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -117,6 +117,7 @@ interface BarChartComponentProps { barChart: ChartSeriesData[] | null | undefined; configs?: ChartSeriesConfigs | undefined; stackByField?: string; + timelineId?: string; } const NO_LEGEND_DATA: LegendItem[] = []; @@ -125,6 +126,7 @@ export const BarChartComponent: React.FC = ({ barChart, configs, stackByField, + timelineId, }) => { const { ref: measureRef, width, height } = useThrottledResizeObserver(); const legendItems: LegendItem[] = useMemo( @@ -135,11 +137,12 @@ export const BarChartComponent: React.FC = ({ dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` ), + timelineId, field: stackByField, value: d.key, })) : NO_LEGEND_DATA, - [barChart, stackByField] + [barChart, stackByField, timelineId] ); const customHeight = get('customHeight', configs); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx index cdda1733932d5..bb71e5e73475d 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx @@ -21,13 +21,14 @@ export interface LegendItem { color?: string; dataProviderId: string; field: string; + timelineId?: string; value: string; } const DraggableLegendItemComponent: React.FC<{ legendItem: LegendItem; }> = ({ legendItem }) => { - const { color, dataProviderId, field, value } = legendItem; + const { color, dataProviderId, field, timelineId, value } = legendItem; return ( @@ -44,6 +45,7 @@ const DraggableLegendItemComponent: React.FC<{ data-test-subj={`legend-item-${dataProviderId}`} field={field} id={dataProviderId} + timelineId={timelineId} value={value} /> ) : ( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 3edc1d0d84b69..74efe2d34fcca 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -18,11 +18,10 @@ import { IdToDataProvider } from '../../store/drag_and_drop/model'; import { State } from '../../store/types'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { reArrangeProviders } from '../../../timelines/components/timeline/data_providers/helpers'; -import { ACTIVE_TIMELINE_REDUX_ID } from '../top_n'; import { ADDED_TO_TIMELINE_MESSAGE } from '../../hooks/translations'; import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; import { displaySuccessToast, useStateToaster } from '../toasters'; - +import { TimelineId } from '../../../../common/types/timeline'; import { addFieldToTimelineColumns, addProviderToTimeline, @@ -35,7 +34,7 @@ import { userIsReArrangingProviders, } from './helpers'; -// @ts-ignore +// @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; interface Props { @@ -67,7 +66,7 @@ const onDragEndHandler = ({ destination: result.destination, dispatch, source: result.source, - timelineId: ACTIVE_TIMELINE_REDUX_ID, + timelineId: TimelineId.active, }); } else if (providerWasDroppedOnTimeline(result)) { addProviderToTimeline({ @@ -76,7 +75,7 @@ const onDragEndHandler = ({ dispatch, onAddedToTimeline, result, - timelineId: ACTIVE_TIMELINE_REDUX_ID, + timelineId: TimelineId.active, }); } else if (fieldWasDroppedOnTimelineColumns(result)) { addFieldToTimelineColumns({ @@ -130,7 +129,6 @@ export const DragDropContextWrapperComponent = React.memo {children} @@ -152,7 +150,7 @@ const emptyActiveTimelineDataProviders: DataProvider[] = []; // stable reference const mapStateToProps = (state: State) => { const activeTimelineDataProviders = - timelineSelectors.getTimelineByIdSelector()(state, ACTIVE_TIMELINE_REDUX_ID)?.dataProviders ?? + timelineSelectors.getTimelineByIdSelector()(state, TimelineId.active)?.dataProviders ?? emptyActiveTimelineDataProviders; const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 22b95f0d0c0e9..e7594365e8103 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Draggable, DraggableProvided, @@ -22,7 +22,7 @@ import { DataProvider } from '../../../timelines/components/timeline/data_provid import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; -import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; +import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -76,6 +76,7 @@ interface Props { dataProvider: DataProvider; inline?: boolean; render: RenderFunctionProp; + timelineId?: string; truncate?: boolean; onFilterAdded?: () => void; } @@ -100,16 +101,31 @@ export const getStyle = ( }; export const DraggableWrapper = React.memo( - ({ dataProvider, onFilterAdded, render, truncate }) => { + ({ dataProvider, onFilterAdded, render, timelineId, truncate }) => { + const draggableRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); const [showTopN, setShowTopN] = useState(false); - const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); - + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); const [providerRegistered, setProviderRegistered] = useState(false); const dispatch = useDispatch(); + const handleClosePopOverTrigger = useCallback( + () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), + [] + ); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + if (newShowTopN === false) { + handleClosePopOverTrigger(); + } + return newShowTopN; + }); + }, [handleClosePopOverTrigger]); + const registerProvider = useCallback(() => { if (!providerRegistered) { dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); @@ -126,17 +142,19 @@ export const DraggableWrapper = React.memo( () => () => { unRegisterProvider(); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [unRegisterProvider] ); const hoverContent = useMemo( () => ( ( } /> ), - [dataProvider, onFilterAdded, showTopN, toggleTopN] + [ + dataProvider, + handleClosePopOverTrigger, + onFilterAdded, + showTopN, + timelineId, + timelineIdFind, + toggleTopN, + ] ); const renderContent = useCallback( @@ -184,7 +210,10 @@ export const DraggableWrapper = React.memo( { + provided.innerRef(e); + draggableRef.current = e; + }} data-test-subj="providerContainer" isDragging={snapshot.isDragging} registerProvider={registerProvider} @@ -214,7 +243,12 @@ export const DraggableWrapper = React.memo( ); return ( - + ); }, (prevProps, nextProps) => diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index ee1dc73b27fe2..3507b0f8c447d 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -52,6 +52,7 @@ jest.mock('../../../timelines/components/manage_timeline', () => { return { ...original, useManageTimeline: () => ({ + getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }), getTimelineFilterManager: mockGetTimelineFilterManager, isManagedTimeline: jest.fn().mockReturnValue(false), }), @@ -63,8 +64,10 @@ const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; const toggleTopN = jest.fn(); +const goGetTimelineId = jest.fn(); const defaultProps = { field, + goGetTimelineId, showTopN: false, timelineId, toggleTopN, @@ -130,6 +133,18 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() ).toBe(false); }); + + test(`it should call goGetTimelineId when user is over the 'Filter ${hoverAction} value' button`, () => { + const wrapper = mount( + + + + ); + const button = wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first(); + button.simulate('mouseenter'); + expect(goGetTimelineId).toHaveBeenCalledWith(true); + }); + describe('when run in the context of a timeline', () => { let wrapper: ReactWrapper; let onFilterAdded: () => void; @@ -151,6 +166,7 @@ describe('DraggableWrapperHoverContent', () => { ); }); + test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); @@ -459,6 +475,24 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); }); + test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + ); + const button = wrapper.find(`[data-test-subj="show-top-field"]`).first(); + button.simulate('mouseenter'); + expect(goGetTimelineId).toHaveBeenCalledWith(true); + }); + test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 4efdea5eee43b..a951bfa98d64b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; import { getAllFieldsByName, useWithSource } from '../../containers/source'; @@ -19,20 +19,25 @@ import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; +import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles'; interface Props { + closePopOver?: () => void; draggableId?: DraggableId; field: string; + goGetTimelineId?: (args: boolean) => void; onFilterAdded?: () => void; showTopN: boolean; - timelineId?: string; + timelineId?: string | null; toggleTopN: () => void; value?: string[] | string | null; } const DraggableWrapperHoverContentComponent: React.FC = ({ + closePopOver, draggableId, field, + goGetTimelineId, onFilterAdded, showTopN, timelineId, @@ -44,17 +49,37 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - const { getTimelineFilterManager } = useManageTimeline(); + const { getManageTimelineById, getTimelineFilterManager } = useManageTimeline(); const filterManager = useMemo( () => - timelineId === TimelineId.active || - (draggableId != null && draggableId?.includes(TimelineId.active)) + timelineId === TimelineId.active ? getTimelineFilterManager(TimelineId.active) : filterManagerBackup, - [draggableId, timelineId, getTimelineFilterManager, filterManagerBackup] + [timelineId, getTimelineFilterManager, filterManagerBackup] ); + // Regarding data from useManageTimeline: + // * `indexToAdd`, which enables the alerts index to be appended to + // the `indexPattern` returned by `useWithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the alerts index + // to the index pattern. + const { indexToAdd } = useMemo( + () => + timelineId === TimelineId.active + ? getManageTimelineById(TimelineId.active) + : { indexToAdd: null }, + [getManageTimelineById, timelineId] + ); + + const handleStartDragToTimeline = useCallback(() => { + startDragToTimeline(); + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, startDragToTimeline]); + const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); @@ -62,13 +87,15 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ if (activeFilterManager != null) { activeFilterManager.addFilters(filter); - + if (closePopOver != null) { + closePopOver(); + } if (onFilterAdded != null) { onFilterAdded(); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field, value, filterManager, onFilterAdded]); + }, [closePopOver, field, value, filterManager, onFilterAdded]); const filterOutValue = useCallback(() => { const filter = @@ -78,14 +105,23 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ if (activeFilterManager != null) { activeFilterManager.addFilters(filter); + if (closePopOver != null) { + closePopOver(); + } if (onFilterAdded != null) { onFilterAdded(); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field, value, filterManager, onFilterAdded]); + }, [closePopOver, field, value, filterManager, onFilterAdded]); - const { browserFields } = useWithSource(); + const handleGoGetTimelineId = useCallback(() => { + if (goGetTimelineId != null && timelineId == null) { + goGetTimelineId(true); + } + }, [goGetTimelineId, timelineId]); + + const { browserFields, indexPattern } = useWithSource('default', indexToAdd); return ( <> @@ -97,6 +133,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="filter-for-value" iconType="magnifyWithPlus" onClick={filterForValue} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -109,6 +146,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="filter-out-value" iconType="magnifyWithMinus" onClick={filterOutValue} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -120,7 +158,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ color="text" data-test-subj="add-to-timeline" iconType="timeline" - onClick={startDragToTimeline} + onClick={handleStartDragToTimeline} /> )} @@ -139,6 +177,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="show-top-field" iconType="visBarVertical" onClick={toggleTopN} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -147,7 +186,10 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ @@ -172,3 +214,30 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); + +export const useGetTimelineId = function ( + elem: React.MutableRefObject, + getTimelineId: boolean = false +) { + const [timelineId, setTimelineId] = useState(null); + + useEffect(() => { + let startElem: Element | (Node & ParentNode) | null = elem.current; + if (startElem != null && getTimelineId) { + for (; startElem && startElem !== document; startElem = startElem.parentNode) { + const myElem: Element = startElem as Element; + if ( + myElem != null && + myElem.classList != null && + myElem.classList.contains(SELECTOR_TIMELINE_BODY_CLASS_NAME) && + myElem.hasAttribute('data-timeline-id') + ) { + setTimelineId(myElem.getAttribute('data-timeline-id')); + break; + } + } + } + }, [elem, getTimelineId]); + + return timelineId; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index fcf007a4cf1ba..62a07550650aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -21,6 +21,7 @@ export interface DefaultDraggableType { name?: string | null; queryValue?: string | null; children?: React.ReactNode; + timelineId?: string; tooltipContent?: React.ReactNode; } @@ -83,7 +84,7 @@ Content.displayName = 'Content'; * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, tooltipContent, queryValue }) => + ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => value != null ? ( ( ) } + timelineId={timelineId} /> ) : null ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index e01ccf1e544bb..7b6e9fb21a3e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -147,7 +147,6 @@ export const getColumns = ({ data-test-subj="field-name" fieldId={field} onUpdateColumns={onUpdateColumns} - timelineId={contextId} />
)} diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 3e196c4b7bad4..31f7e1b7fac7c 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -53,6 +53,7 @@ export interface OwnProps extends QueryTemplateProps { showLegend?: boolean; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; + timelineId?: string; title: string | GetTitle; type: hostsModel.HostsType | networkModel.NetworkType; } @@ -94,6 +95,7 @@ export const MatrixHistogramComponent: React.FC< stackByOptions, startDate, subtitle, + timelineId, title, titleSize, dispatchSetAbsoluteRangeDatePicker, @@ -242,6 +244,7 @@ export const MatrixHistogramComponent: React.FC< barChart={barChartData} configs={barchartConfigs} stackByField={selectedStackByOption.value} + timelineId={timelineId} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index a9e6cdd19bb20..f388409b443db 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -80,11 +80,12 @@ export interface MatrixHistogramQueryProps { } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + legendPosition?: Position; scaleType?: ScaleType; - yTickFormatter?: (value: number) => string; showLegend?: boolean; showSpacer?: boolean; - legendPosition?: Position; + timelineId?: string; + yTickFormatter?: (value: number) => string; } export interface HistogramBucket { diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 336f906b3bed0..503e9983692f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -15,17 +15,19 @@ import { SUB_PLUGINS_REDUCER, kibanaObservable, createSecuritySolutionStorageMock, + mockIndexPattern, } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { createStore, State } from '../../store'; import { Props } from './top_n'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; +import { StatefulTopN } from '.'; import { ManageGlobalTimeline, timelineDefaults, } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -94,9 +96,9 @@ const state: State = { timeline: { ...mockGlobalState.timeline, timelineById: { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...mockGlobalState.timeline.timelineById.test, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, dataProviders: [ { id: @@ -189,6 +191,9 @@ describe('StatefulTopN', () => { { beforeEach(() => { filterManager = new FilterManager(mockUiSettingsForFilterManager); const manageTimelineForTesting = { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...timelineDefaults, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, filterManager, }, }; @@ -278,6 +283,9 @@ describe('StatefulTopN', () => { { const filterManager = new FilterManager(mockUiSettingsForFilterManager); const manageTimelineForTesting = { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...timelineDefaults, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, filterManager, documentType: 'alerts', }, @@ -356,6 +364,9 @@ describe('StatefulTopN', () => { { // filters that appear at the top of most views in the app, and all the // filters in the active timeline: const mapStateToProps = (state: State) => { - const activeTimeline: TimelineModel = - getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimeline: TimelineModel = getTimeline(state, TimelineId.active) ?? timelineDefaults; const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); @@ -48,7 +49,7 @@ const makeMapStateToProps = () => { activeTimelineEventType: activeTimeline.eventType, activeTimelineFilters, activeTimelineFrom: activeTimelineInput.timerange.from, - activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, TimelineId.active), activeTimelineTo: activeTimelineInput.timerange.to, dataProviders: activeTimeline.dataProviders, globalQuery: getGlobalQuerySelector(state), @@ -64,9 +65,17 @@ const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRang const connector = connect(makeMapStateToProps, mapDispatchToProps); +// * `indexToAdd`, which enables the alerts index to be appended to +// the `indexPattern` returned by `useWithSource`, may only be populated when +// this component is rendered in the context of the active timeline. This +// behavior enables the 'All events' view by appending the alerts index +// to the index pattern. interface OwnProps { browserFields: BrowserFields; field: string; + indexPattern: IIndexPattern; + indexToAdd: string[] | null; + timelineId?: string; toggleTopN: () => void; onFilterAdded?: () => void; value?: string[] | string | null; @@ -83,48 +92,29 @@ const StatefulTopNComponent: React.FC = ({ browserFields, dataProviders, field, + indexPattern, + indexToAdd, globalFilters = EMPTY_FILTERS, globalQuery = EMPTY_QUERY, kqlMode, onFilterAdded, setAbsoluteRangeDatePicker, + timelineId, toggleTopN, value, }) => { const kibana = useKibana(); - // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'alerts') may only be populated in some views, - // e.g. the `Alerts` view on the `Detections` page. - // * `id` (`timelineId`) may only be populated when we are rendered in the - // context of the active timeline. - // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `useWithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the alerts index - // to the index pattern. - const { isManagedTimeline, getManageTimelineById } = useManageTimeline(); - const { documentType, id: timelineId, indexToAdd } = useMemo( - () => - isManagedTimeline(ACTIVE_TIMELINE_REDUX_ID) - ? getManageTimelineById(ACTIVE_TIMELINE_REDUX_ID) - : { documentType: null, id: null, indexToAdd: null }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [getManageTimelineById] - ); - const options = getOptions( - timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + timelineId === TimelineId.active ? activeTimelineEventType : undefined ); - const { indexPattern } = useWithSource('default', indexToAdd); - return ( {({ from, deleteQuery, setQuery, to }) => ( = ({ : undefined } data-test-subj="top-n" - defaultView={documentType?.toLocaleLowerCase() === 'alerts' ? 'alert' : options[0].value} - deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} + defaultView={ + timelineId === TimelineId.alertsPage || timelineId === TimelineId.alertsRulesDetailsPage + ? 'alert' + : options[0].value + } + deleteQuery={timelineId === TimelineId.active ? undefined : deleteQuery} field={field} - filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} - from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} + filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters} + from={timelineId === TimelineId.active ? activeTimelineFrom : from} indexPattern={indexPattern} indexToAdd={indexToAdd} options={options} - query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} + query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget={ - timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' + timelineId === TimelineId.active ? 'timeline' : 'global' } setQuery={setQuery} - to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} + timelineId={timelineId} + to={timelineId === TimelineId.active ? activeTimelineTo : to} toggleTopN={toggleTopN} onFilterAdded={onFilterAdded} value={value} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 0ccb7e1e72f1f..7d19bf21271aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -67,6 +67,7 @@ export interface Props { refetch: inputsModel.Refetch; }) => void; to: number; + timelineId?: string; toggleTopN: () => void; onFilterAdded?: () => void; value?: string[] | string | null; @@ -89,12 +90,17 @@ const TopNComponent: React.FC = ({ setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget, setQuery, + timelineId, to, toggleTopN, }) => { const [view, setView] = useState(defaultView); const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]); + useEffect(() => { + setView(defaultView); + }, [defaultView]); + const headerChildren = useMemo( () => ( = ({ setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={false} + timelineId={timelineId} to={to} /> ) : ( @@ -145,6 +152,7 @@ const TopNComponent: React.FC = ({ setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} + timelineId={timelineId} to={to} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index 8679dae448332..361779a4a33b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -5,7 +5,7 @@ */ import { EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; @@ -22,6 +22,7 @@ interface Props { * Always show the hover menu contents (default: false) */ alwaysShow?: boolean; + closePopOverTrigger?: boolean; /** * The contents of the hover menu. It is highly recommended you wrap this * content in a `div` with `position: absolute` to prevent it from effecting @@ -47,7 +48,8 @@ interface Props { * provides a signal to the content that the user is in a hover state. */ export const WithHoverActions = React.memo( - ({ alwaysShow = false, hoverContent, render }) => { + ({ alwaysShow = false, closePopOverTrigger, hoverContent, render }) => { + const [isOpen, setIsOpen] = useState(hoverContent != null && alwaysShow); const [showHoverContent, setShowHoverContent] = useState(false); const onMouseEnter = useCallback(() => { // NOTE: the following read from the DOM is expensive, but not as @@ -64,10 +66,16 @@ export const WithHoverActions = React.memo( const content = useMemo(() => <>{render(showHoverContent)}, [render, showHoverContent]); - const isOpen = hoverContent != null && (showHoverContent || alwaysShow); + useEffect(() => { + setIsOpen(hoverContent != null && (showHoverContent || alwaysShow)); + }, [hoverContent, showHoverContent, alwaysShow]); - const popover = useMemo(() => { - return ( + useEffect(() => { + setShowHoverContent(false); + }, [closePopOverTrigger]); + + return ( +
( isOpen={isOpen} panelPaddingSize={!alwaysShow ? 's' : 'none'} > - {isOpen ? hoverContent : null} + {isOpen ? <>{hoverContent} : null} - ); - }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); - - return ( -
- {popover}
); } diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 9aa3b007511a1..4f42f20c45ae1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -97,7 +97,7 @@ export const useWithSource = ( const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...(!onlyCheckIndexToAdd ? configIndex : []), ...indexToAdd]; + return onlyCheckIndexToAdd ? indexToAdd : [...configIndex, ...indexToAdd]; } return configIndex; }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); @@ -135,41 +135,32 @@ export const useWithSource = ( }, }, }); - if (!isSubscribed) { - return setState((prevState) => ({ - ...prevState, + + if (isSubscribed) { + setState({ loading: false, - })); + indicesExist: indicesExistOrDataTemporarilyUnavailable( + get('data.source.status.indicesExist', result) + ), + browserFields: getBrowserFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + indexPattern: getIndexFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + errorMessage: null, + }); } - - setState({ - loading: false, - indicesExist: indicesExistOrDataTemporarilyUnavailable( - get('data.source.status.indicesExist', result) - ), - browserFields: getBrowserFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - indexPattern: getIndexFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - errorMessage: null, - }); } catch (error) { - if (!isSubscribed) { - return setState((prevState) => ({ + if (isSubscribed) { + setState((prevState) => ({ ...prevState, loading: false, + errorMessage: error.message, })); } - - setState((prevState) => ({ - ...prevState, - loading: false, - errorMessage: error.message, - })); } } diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts index 4e2b11b24e5a9..e84438581fcde 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts @@ -26,33 +26,33 @@ describe('Kuery escape', () => { expect(escapeKuery(value)).to.be(expected); }); - it('should escape keywords', () => { + it('should NOT escape keywords', () => { const value = 'foo and bar or baz not qux'; - const expected = 'foo \\and bar \\or baz \\not qux'; + const expected = 'foo and bar or baz not qux'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape keywords next to each other', () => { + it('should NOT escape keywords next to each other', () => { const value = 'foo and bar or not baz'; - const expected = 'foo \\and bar \\or \\not baz'; + const expected = 'foo and bar or not baz'; expect(escapeKuery(value)).to.be(expected); }); it('should not escape keywords without surrounding spaces', () => { const value = 'And this has keywords, or does it not?'; - const expected = 'And this has keywords, \\or does it not?'; + const expected = 'And this has keywords, or does it not?'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape uppercase keywords', () => { + it('should NOT escape uppercase keywords', () => { const value = 'foo AND bar'; - const expected = 'foo \\AND bar'; + const expected = 'foo AND bar'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape both keywords and special characters', () => { + it('should escape special characters and NOT keywords', () => { const value = 'Hello, "world", and to meet you!'; - const expected = 'Hello, \\"world\\", \\and to meet you!'; + const expected = 'Hello, \\"world\\", and to meet you!'; expect(escapeKuery(value)).to.be(expected); }); diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index bd4d96a98c815..b06a6ec10f48e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -75,11 +75,12 @@ const escapeWhitespace = (val: string) => const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string // See the Keyword rule in kuery.peg -const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); +// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; +// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); -const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); +// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); -export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); +export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); export const convertToBuildEsQuery = ({ config, diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index fe3f9f8ecda33..7d42f744a2613 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -60,6 +60,7 @@ interface Props { refetch: inputsModel.Refetch; }) => void; showSpacer?: boolean; + timelineId?: string; to: number; } @@ -81,6 +82,7 @@ const EventsByDatasetComponent: React.FC = ({ setAbsoluteRangeDatePickerTarget, setQuery, showSpacer = true, + timelineId, to, }) => { // create a unique, but stable (across re-renders) query id @@ -177,6 +179,7 @@ const EventsByDatasetComponent: React.FC = ({ showSpacer={showSpacer} sourceId="default" startDate={from} + timelineId={timelineId} type={HostsType.page} {...eventsByDatasetHistogramConfigs} title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 5010fd9c06eb7..41152dabe2ad8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -37,6 +37,7 @@ interface Props { loading: boolean; refetch: inputsModel.Refetch; }) => void; + timelineId?: string; to: number; } @@ -50,6 +51,7 @@ const SignalsByCategoryComponent: React.FC = ({ setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget = 'global', setQuery, + timelineId, to, }) => { const { signalIndexName } = useSignalIndex(); @@ -83,6 +85,7 @@ const SignalsByCategoryComponent: React.FC = ({ showLinkToAlerts={onlyField == null ? true : false} stackByOptions={onlyField == null ? alertsHistogramOptions : undefined} legendPosition={'right'} + timelineId={timelineId} to={to} title={i18n.ALERT_COUNT} updateDateRange={updateDateRangeCallback} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx index aaad9cf145ab7..8922434746234 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx @@ -152,7 +152,6 @@ export const getFieldItems = ({ fieldId={field.name || ''} highlight={highlight} onUpdateColumns={onUpdateColumns} - timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index da0cbb99b8671..1f917c664e813 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -24,7 +24,6 @@ const defaultProps = { }), fieldId: timestampFieldId, onUpdateColumns: jest.fn(), - timelineId: 'timeline-id', }; describe('FieldName', () => { @@ -46,8 +45,7 @@ describe('FieldName', () => { ); - - wrapper.simulate('mouseenter'); + wrapper.find('div').at(1).simulate('mouseenter'); wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx index 985c8b35094ef..62e41d967cb9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx @@ -5,13 +5,16 @@ */ import { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { DraggableWrapperHoverContent } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; +import { + DraggableWrapperHoverContent, + useGetTimelineId, +} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; /** * The name of a (draggable) field @@ -77,23 +80,34 @@ export const FieldName = React.memo<{ fieldId: string; highlight?: string; onUpdateColumns: OnUpdateColumns; - timelineId: string; -}>(({ fieldId, highlight = '', timelineId }) => { +}>(({ fieldId, highlight = '' }) => { + const containerRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); const [showTopN, setShowTopN] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(containerRef, goGetTimelineId); + const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); + setShowTopN((prevShowTopN) => !prevShowTopN); + }, []); + + const handleClosePopOverTrigger = useCallback( + () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), + [] + ); const hoverContent = useMemo( () => ( ), - [fieldId, showTopN, toggleTopN, timelineId] + [fieldId, handleClosePopOverTrigger, showTopN, timelineIdFind, toggleTopN] ); const render = useCallback( @@ -109,7 +123,16 @@ export const FieldName = React.memo<{ [fieldId, highlight] ); - return ; + return ( +
+ +
+ ); }); FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index be655f7465a1b..3b40c36fccd16 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -57,6 +57,11 @@ type ActionManageTimeline = id: string; payload: boolean; } + | { + type: 'SET_INDEX_TO_ADD'; + id: string; + payload: string[]; + } | { type: 'SET_TIMELINE_ACTIONS'; id: string; @@ -81,7 +86,10 @@ export const timelineDefaults = { title: i18n.EVENTS, unit: (n: number) => i18n.UNIT(n), }; -const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTimeline) => { +const reducerManageTimeline = ( + state: ManageTimelineById, + action: ActionManageTimeline +): ManageTimelineById => { switch (action.type) { case 'INITIALIZE_TIMELINE': return { @@ -91,7 +99,15 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], ...action.payload, }, - }; + } as ManageTimelineById; + case 'SET_INDEX_TO_ADD': + return { + ...state, + [action.id]: { + ...state[action.id], + indexToAdd: action.payload, + }, + } as ManageTimelineById; case 'SET_TIMELINE_ACTIONS': case 'SET_TIMELINE_FILTER_MANAGER': return { @@ -100,7 +116,7 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], ...action.payload, }, - }; + } as ManageTimelineById; case 'SET_IS_LOADING': return { ...state, @@ -108,7 +124,7 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], isLoading: action.payload, }, - }; + } as ManageTimelineById; default: return state; } @@ -119,6 +135,7 @@ interface UseTimelineManager { getTimelineFilterManager: (id: string) => FilterManager | undefined; initializeTimeline: (newTimeline: ManageTimelineInit) => void; isManagedTimeline: (id: string) => boolean; + setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setTimelineRowActions: (actionsArgs: { id: string; @@ -129,10 +146,9 @@ interface UseTimelineManager { } const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseTimelineManager => { - const [state, dispatch] = useReducer( - reducerManageTimeline, - manageTimelineForTesting ?? initManageTimeline - ); + const [state, dispatch] = useReducer< + (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById + >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline); const initializeTimeline = useCallback((newTimeline: ManageTimelineInit) => { dispatch({ @@ -183,8 +199,16 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT [] ); + const setIndexToAdd = useCallback(({ id, indexToAdd }: { id: string; indexToAdd: string[] }) => { + dispatch({ + type: 'SET_INDEX_TO_ADD', + id, + payload: indexToAdd, + }); + }, []); + const getTimelineFilterManager = useCallback( - (id: string): FilterManager | undefined => state[id].filterManager, + (id: string): FilterManager | undefined => state[id]?.filterManager, [state] ); const getManageTimelineById = useCallback( @@ -195,8 +219,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT initializeTimeline({ id }); return { ...timelineDefaults, id }; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [state] + [initializeTimeline, state] ); const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]); @@ -205,6 +228,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT getTimelineFilterManager, initializeTimeline, isManagedTimeline, + setIndexToAdd, setIsTimelineLoading, setTimelineRowActions, setTimelineFilterManager, @@ -214,6 +238,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT const init = { getManageTimelineById: (id: string) => ({ ...timelineDefaults, id }), getTimelineFilterManager: () => undefined, + setIndexToAdd: () => undefined, isManagedTimeline: () => false, initializeTimeline: () => noop, setIsTimelineLoading: () => noop, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 51bf883ed2d61..43ea5e905ca8b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -17,6 +17,7 @@ import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { wait } from '../../../../common/lib/helpers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; +import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../styles'; const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; @@ -133,6 +134,20 @@ describe('Body', () => { ).toEqual(true); }); }, 20000); + + test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_BODY_CLASS_NAME}`, () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`) + .first() + .exists() + ).toEqual(true); + }); }); describe('action on event', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 46895c86de084..6a296170fffde 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -139,6 +139,7 @@ export const Body = React.memo( )} ( pinnedEventIds={pinnedEventIds} rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]} selectedEventIds={selectedEventIds} - show={id === ACTIVE_TIMELINE_REDUX_ID ? show : true} + show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index d1a58dafcb328..47d848021ba43 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -14,16 +14,17 @@ import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/component /** * TIMELINE BODY */ +export const SELECTOR_TIMELINE_BODY_CLASS_NAME = 'securitySolutionTimeline__body'; // SIDE EFFECT: the following creates a global class selector export const TimelineBodyGlobalStyle = createGlobalStyle` - body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .siemTimeline__body { + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .${SELECTOR_TIMELINE_BODY_CLASS_NAME} { overflow: hidden; } `; export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ - className: `siemTimeline__body ${className}`, + className: `${SELECTOR_TIMELINE_BODY_CLASS_NAME} ${className}`, }))<{ bodyHeight?: number; visible: boolean }>` height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; overflow: auto; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index 07d4b004d2eda..18deaf0158723 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -174,6 +174,7 @@ export const TimelineComponent: React.FC = ({ const [isQueryLoading, setIsQueryLoading] = useState(false); const { initializeTimeline, + setIndexToAdd, setIsTimelineLoading, setTimelineFilterManager, setTimelineRowActions, @@ -188,12 +189,14 @@ export const TimelineComponent: React.FC = ({ }, []); useEffect(() => { setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingIndexName }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadingIndexName, isQueryLoading]); + }, [loadingIndexName, id, isQueryLoading, setIsTimelineLoading]); useEffect(() => { setTimelineFilterManager({ id, filterManager }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterManager]); + }, [filterManager, id, setTimelineFilterManager]); + + useEffect(() => { + setIndexToAdd({ id, indexToAdd }); + }, [id, indexToAdd, setIndexToAdd]); return ( From 991fdbd95d9db143a47446b4739d20c9905516d6 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Fri, 3 Jul 2020 15:57:17 -0500 Subject: [PATCH 08/12] [7.x] logout from transform_poweruser user in after method of transform tests (#70644) (#70738) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/transform/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 04a751279bf3c..6dd22a1f4a204 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -5,9 +5,10 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { +export default function ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); + const PageObjects = getPageObjects(['security']); describe('transform', function () { this.tags(['ciGroup9', 'transform']); @@ -30,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/ecommerce'); await transform.testResources.resetKibanaTimeZone(); + await PageObjects.security.logout(); }); loadTestFile(require.resolve('./creation_index_pattern')); From 829d37856d3d26e9b2e1c3982ea1b68e5af15807 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Sat, 4 Jul 2020 01:26:12 +0200 Subject: [PATCH 09/12] [functional tests] test url field formatter on dashboard and discover (#70736) (#70747) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- test/functional/apps/dashboard/index.js | 1 + .../apps/dashboard/url_field_formatter.ts | 91 +++++++++++++++++++ .../page_objects/visualize_chart_page.ts | 14 +++ 3 files changed, 106 insertions(+) create mode 100644 test/functional/apps/dashboard/url_field_formatter.ts diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 1e310c1ddd268..5a30456bd59ab 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -49,6 +49,7 @@ export default function ({ getService, loadTestFile }) { after(unloadCurrentData); loadTestFile(require.resolve('./empty_dashboard')); + loadTestFile(require.resolve('./url_field_formatter')); loadTestFile(require.resolve('./embeddable_rendering')); loadTestFile(require.resolve('./create_and_add_embeddables')); loadTestFile(require.resolve('./edit_embeddable_redirects')); diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts new file mode 100644 index 0000000000000..9b05b9b777b94 --- /dev/null +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, dashboard, settings, timePicker, visChart } = getPageObjects([ + 'common', + 'dashboard', + 'settings', + 'timePicker', + 'visChart', + ]); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const fieldName = 'clientip'; + + const clickFieldAndCheckUrl = async (fieldLink: WebElementWrapper) => { + const fieldValue = await fieldLink.getVisibleText(); + await fieldLink.click(); + const windowHandlers = await browser.getAllWindowHandles(); + expect(windowHandlers.length).to.equal(2); + await browser.switchToWindow(windowHandlers[1]); + const currentUrl = await browser.getCurrentUrl(); + const fieldUrl = common.getHostPort() + '/app/' + fieldValue; + expect(currentUrl).to.equal(fieldUrl); + }; + + describe('Changing field formatter to Url', () => { + before(async function () { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await common.navigateToApp('settings'); + await settings.clickKibanaIndexPatterns(); + await settings.clickIndexPatternLogstash(); + await settings.filterField(fieldName); + await settings.openControlsByName(fieldName); + await settings.setFieldFormat('url'); + await settings.controlChangeSave(); + }); + + it('applied on dashboard', async () => { + await common.navigateToApp('dashboard'); + await dashboard.loadSavedDashboard('dashboard with everything'); + await dashboard.waitForRenderComplete(); + const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`, 1); + await clickFieldAndCheckUrl(fieldLink); + }); + + it('applied on discover', async () => { + await common.navigateToApp('discover'); + await timePicker.setAbsoluteRange( + 'Sep 19, 2017 @ 06:31:44.000', + 'Sep 23, 2018 @ 18:31:44.000' + ); + await testSubjects.click('docTableExpandToggleColumn'); + const fieldLink = await testSubjects.find(`tableDocViewRow-${fieldName}-value`); + await clickFieldAndCheckUrl(fieldLink); + }); + + afterEach(async function () { + const windowHandlers = await browser.getAllWindowHandles(); + if (windowHandlers.length > 1) { + await browser.closeCurrentWindow(); + await browser.switchToWindow(windowHandlers[0]); + } + }); + }); +} diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 673fba0c346b8..590631ad48b00 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -302,6 +302,20 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return element.getVisibleText(); } + public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { + const tableVis = await testSubjects.find('tableVis'); + const $ = await tableVis.parseDomContent(); + const headers = $('span[ng-bind="::col.title"]') + .toArray() + .map((header: any) => $(header).text()); + const fieldColumnIndex = headers.indexOf(fieldName); + return await find.byCssSelector( + `[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${ + fieldColumnIndex + 1 + }) a` + ); + } + /** * If you are writing new tests, you should rather look into getTableVisContent method instead. * @deprecated Use getTableVisContent instead. From 169d06c55b869a5ffcc52f56313a9abb6e3c76bc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Sat, 4 Jul 2020 17:31:08 +0200 Subject: [PATCH 10/12] [Ingest Pipelines] Load from json (#70297) (#70751) * WiP load from json modal ready, need to refactor more stuff * First iteration of load from JSON functionality - refactored the pipeline processsors editor components for portability - added CIT for load from json component * added comment * update deserialize with tests and make it more fault tolerant * use flyout footer * remove console.error and make the json editor a lot shorter * address PR feedback - Update form schema and form schema types - simplify the save handler - refactor processors_title to processors_header * remove unused translations Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../on_failure_processors_title.tsx | 2 +- .../pipeline_form.scss} | 0 .../pipeline_form/pipeline_form.tsx | 65 +++-- .../pipeline_form/pipeline_form_fields.tsx | 78 +++--- .../processors_header.tsx} | 13 +- .../components/pipeline_form/schema.ts | 44 ++++ .../components/pipeline_form/schema.tsx | 138 ---------- .../components/pipeline_form/types.ts | 2 + .../pipeline_processors_editor/README.md | 24 ++ .../pipeline_processors_editor.helpers.tsx | 24 +- .../pipeline_processors_editor.test.tsx | 14 +- .../components/index.ts | 8 +- .../components/load_from_json/button.tsx | 34 +++ .../components/load_from_json/index.ts | 8 + .../load_from_json/modal_provider.test.tsx | 127 ++++++++++ .../load_from_json/modal_provider.tsx | 138 ++++++++++ .../components/pipeline_processors_editor.tsx | 33 +++ .../context_menu.tsx | 10 +- .../{messages.ts => i18n_texts.ts} | 2 +- .../pipeline_processors_editor_item.tsx | 22 +- .../processor_settings_form.tsx | 44 ++-- .../pipeline_processors_editor/constants.ts | 5 + .../pipeline_processors_editor/context.tsx | 221 +++++++++++++++- .../deserialize.test.ts | 74 ++++++ .../pipeline_processors_editor/deserialize.ts | 8 +- .../global_on_failure_processors_editor.tsx | 13 + .../editors/index.ts | 8 + .../editors/processors_editor.tsx | 13 + .../pipeline_processors_editor/index.ts | 8 +- .../pipeline_processors_editor.container.tsx | 74 ------ .../pipeline_processors_editor.tsx | 239 ------------------ .../processors_reducer/index.ts | 2 +- .../processors_reducer/processors_reducer.ts | 14 + .../pipeline_processors_editor/types.ts | 2 + .../ingest_pipelines/public/shared_imports.ts | 2 + .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 37 files changed, 931 insertions(+), 594 deletions(-) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/components => pipeline_form}/on_failure_processors_title.tsx (95%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/pipeline_processors_editor.scss => pipeline_form/pipeline_form.scss} (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/components/processors_title_and_test_button.tsx => pipeline_form/processors_header.tsx} (84%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/{messages.ts => i18n_texts.ts} (98%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx similarity index 95% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx index 251a2ffe95212..d223653442819 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { links } = usePipelineProcessorsContext(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index a68e667f4ab43..341e15132d353 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -9,19 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useForm, Form, FormConfig } from '../../../shared_imports'; -import { Pipeline } from '../../../../common/types'; +import { Pipeline, Processor } from '../../../../common/types'; -import { - OnUpdateHandlerArg, - OnUpdateHandler, - SerializeResult, -} from '../pipeline_processors_editor'; +import './pipeline_form.scss'; + +import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; +import { PipelineForm as IPipelineForm } from './types'; export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; @@ -32,14 +31,15 @@ export interface PipelineFormProps { isEditing?: boolean; } +const defaultFormValue: Pipeline = Object.freeze({ + name: '', + description: '', + processors: [], + on_failure: [], +}); + export const PipelineForm: React.FunctionComponent = ({ - defaultValue = { - name: '', - description: '', - processors: [], - on_failure: [], - version: '', - }, + defaultValue = defaultFormValue, onSave, isSaving, saveError, @@ -50,34 +50,42 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); - const processorStateRef = useRef(); + const { + processors: initialProcessors, + on_failure: initialOnFailureProcessors, + ...defaultFormValues + } = defaultValue; + + const [processorsState, setProcessorsState] = useState<{ + processors: Processor[]; + onFailure?: Processor[]; + }>({ + processors: initialProcessors, + onFailure: initialOnFailureProcessors, + }); - const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { - let override: SerializeResult | undefined; + const processorStateRef = useRef(); + const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { if (!isValid) { return; } if (processorStateRef.current) { - const processorsState = processorStateRef.current; - if (await processorsState.validate()) { - override = processorsState.getData(); - } else { - return; + const state = processorStateRef.current; + if (await state.validate()) { + onSave({ ...formData, ...state.getData() }); } } - - onSave({ ...formData, ...(override || {}) } as Pipeline); }; const handleTestPipelineClick = () => { setIsTestingPipeline(true); }; - const { form } = useForm({ + const { form } = useForm({ schema: pipelineFormSchema, - defaultValue, + defaultValue: defaultFormValues, onSubmit: handleSave, }); @@ -121,9 +129,12 @@ export const PipelineForm: React.FunctionComponent = ({ {/* All form fields */} { + setProcessorsState({ processors, onFailure }); + }} onEditorFlyoutOpen={onEditorFlyoutOpen} - initialProcessors={defaultValue.processors} - initialOnFailureProcessors={defaultValue.on_failure} + processors={processorsState.processors} + onFailure={processorsState.onFailure} onProcessorsUpdate={onProcessorsChangeHandler} hasVersion={Boolean(defaultValue.version)} isTestButtonDisabled={isTestingPipeline || form.isValid === false} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 52d1a77c1df6d..0e7a45e8d07b9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,17 +6,27 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Processor } from '../../../../common/types'; -import { FormDataProvider } from '../../../shared_imports'; -import { PipelineProcessorsEditor, OnUpdateHandler } from '../pipeline_processors_editor'; import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports'; +import { + PipelineProcessorsContextProvider, + GlobalOnFailureProcessorsEditor, + ProcessorsEditor, + OnUpdateHandler, + OnDoneLoadJsonHandler, +} from '../pipeline_processors_editor'; + +import { ProcessorsHeader } from './processors_header'; +import { OnFailureProcessorsTitle } from './on_failure_processors_title'; + interface Props { - initialProcessors: Processor[]; - initialOnFailureProcessors?: Processor[]; + processors: Processor[]; + onFailure?: Processor[]; + onLoadJson: OnDoneLoadJsonHandler; onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; isTestButtonDisabled: boolean; @@ -29,8 +39,9 @@ const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); export const PipelineFormFields: React.FunctionComponent = ({ - initialProcessors, - initialOnFailureProcessors, + processors, + onFailure, + onLoadJson, onProcessorsUpdate, isEditing, hasVersion, @@ -113,30 +124,37 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* Pipeline Processors Editor */} - - {({ processors, on_failure: onFailure }) => { - const processorProp = - typeof processors === 'string' && processors - ? JSON.parse(processors) - : initialProcessors ?? []; - - const onFailureProp = - typeof onFailure === 'string' && onFailure - ? JSON.parse(onFailure) - : initialOnFailureProcessors ?? []; - return ( - - ); - }} - + +
+ + + + + + + + + + + + + + + + + +
+
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx similarity index 84% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx index 6d1e2610b5c2b..5e5cddbd36b92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx @@ -9,22 +9,26 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; + +import { LoadFromJsonButton, OnDoneLoadJsonHandler } from '../pipeline_processors_editor'; export interface Props { onTestPipelineClick: () => void; isTestButtonDisabled: boolean; + onLoadJson: OnDoneLoadJsonHandler; } -export const ProcessorsTitleAndTestButton: FunctionComponent = ({ +export const ProcessorsHeader: FunctionComponent = ({ onTestPipelineClick, isTestButtonDisabled, + onLoadJson, }) => { const { links } = usePipelineProcessorsContext(); return ( @@ -55,6 +59,9 @@ export const ProcessorsTitleAndTestButton: FunctionComponent = ({ /> + + + = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'Name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description (optional)', + }), + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx deleted file mode 100644 index 5435f43a78acf..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ /dev/null @@ -1,138 +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 { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; - -import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; -import { parseJson, stringifyJson } from '../../lib'; - -const { emptyField, isJsonField } = fieldValidators; -const { toInt } = fieldFormatters; - -export const pipelineFormSchema: FormSchema = { - name: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { - defaultMessage: 'Name', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { - defaultMessage: 'Name is required.', - }) - ), - }, - ], - }, - description: { - type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { - defaultMessage: 'Description (optional)', - }), - }, - processors: { - label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { - defaultMessage: 'Processors', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: 'foo', - value: 'bar', - }, - }, - ])} - - ), - }} - /> - ), - serializer: parseJson, - deserializer: stringifyJson, - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { - defaultMessage: 'Processors are required.', - }) - ), - }, - { - validator: isJsonField( - i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - ), - }, - ], - }, - on_failure: { - label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { - defaultMessage: 'Failure processors (optional)', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ])} - - ), - }} - /> - ), - serializer: (value) => { - const result = parseJson(value); - // If an empty array was passed, strip out this value entirely. - if (!result.length) { - return undefined; - } - return result; - }, - deserializer: stringifyJson, - validations: [ - { - validator: (validationArg) => { - if (!validationArg.value) { - return; - } - return isJsonField( - i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - )(validationArg); - }, - }, - ], - }, - version: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { - defaultMessage: 'Version (optional)', - }), - formatters: [toInt], - }, -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts index bd74f09546ff4..aa52c14e61eae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts @@ -7,3 +7,5 @@ import { Pipeline } from '../../../../common/types'; export type ReadProcessorsFunction = () => Pick; + +export type PipelineForm = Omit; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md new file mode 100644 index 0000000000000..d29af67d3179c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md @@ -0,0 +1,24 @@ +# Pipeline Processors Editor + +This component provides a way to visually build and manage an ingest +pipeline. + +# API + +## Editor components + +The top-level API consists of 3 pieces that enable the maximum amount +of flexibility for consuming code to determine overall layout. + +- PipelineProcessorsEditorContext +- ProcessorsEditor +- GlobalOnFailureProcessorsEditor + +The editor components must be wrapped inside of the context component +as this is where the shared processors state is contained. + +## Load JSON button + +This component is totally standalone. It gives users a button that +presents a modal for loading a pipeline. It does some basic +validation on the JSON to ensure that it is correct. diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index 7ad9aed3c44a4..cc3817d92d5e3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -6,7 +6,12 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; -import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container'; +import { + PipelineProcessorsContextProvider, + Props, + ProcessorsEditor, + GlobalOnFailureProcessorsEditor, +} from '../'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -55,9 +60,16 @@ jest.mock('react-virtualized', () => { }; }); -const testBedSetup = registerTestBed(PipelineProcessorsEditor, { - doMountAsync: false, -}); +const testBedSetup = registerTestBed( + (props: Props) => ( + + + + ), + { + doMountAsync: false, + } +); export interface SetupResult extends TestBed { actions: ReturnType; @@ -146,10 +158,6 @@ const createActions = (testBed: TestBed) => { find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); }); }, - - toggleOnFailure() { - find('pipelineEditorOnFailureToggle').simulate('click'); - }, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index 15121cc71c321..a4bbf840dff71 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -43,9 +43,9 @@ describe('Pipeline Editor', () => { }, onFlyoutOpen: jest.fn(), onUpdate, - isTestButtonDisabled: false, - onTestPipelineClick: jest.fn(), - esDocsBasePath: 'test', + links: { + esDocsBasePath: 'test', + }, }); }); @@ -57,13 +57,6 @@ describe('Pipeline Editor', () => { expect(arg.getData()).toEqual(testProcessors); }); - it('toggles the on-failure processors tree', () => { - const { actions, exists } = testBed; - expect(exists('pipelineEditorOnFailureTree')).toBe(false); - actions.toggleOnFailure(); - expect(exists('pipelineEditorOnFailureTree')).toBe(true); - }); - describe('processors', () => { it('adds a new processor', async () => { const { actions } = testBed; @@ -169,7 +162,6 @@ describe('Pipeline Editor', () => { it('moves to and from the global on-failure tree', async () => { const { actions } = testBed; - actions.toggleOnFailure(); await actions.addProcessor('onFailure', 'test', { if: '1 == 5' }); actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0'); const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index 2d512a6bfa2ed..de0621b187230 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -12,10 +12,10 @@ export { export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; -export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item'; +export { PipelineProcessorsEditor } from './pipeline_processors_editor'; -export { ProcessorRemoveModal } from './processor_remove_modal'; +export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item'; -export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button'; +export { ProcessorRemoveModal } from './processor_remove_modal'; -export { OnFailureProcessorsTitle } from './on_failure_processors_title'; +export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx new file mode 100644 index 0000000000000..482878d1bda58 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiButton } from '@elastic/eui'; + +import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +interface Props { + onDone: OnDoneLoadJsonHandler; +} + +const i18nTexts = { + buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { + defaultMessage: 'Load JSON', + }), +}; + +export const LoadFromJsonButton: FunctionComponent = ({ onDone }) => { + return ( + + {(openModal) => { + return ( + + {i18nTexts.buttonLabel} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts new file mode 100644 index 0000000000000..c1c49f251d518 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/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 { LoadFromJsonButton } from './button'; +export { OnDoneLoadJsonHandler } from './modal_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx new file mode 100644 index 0000000000000..2f4cdce1edd0b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: any) => fn, + }; +}); + +import { registerTestBed, TestBed } from '../../../../../../../../test_utils/testbed'; + +const setup = ({ onDone }: { onDone: OnDoneLoadJsonHandler }) => { + return registerTestBed( + () => ( + + {(openModal) => { + return ( + + ); + }} + + ), + { + memoryRouter: { + wrapComponent: false, + }, + } + )(); +}; + +describe('Load from JSON ModalProvider', () => { + let testBed: TestBed; + let onDone: jest.Mock; + + beforeEach(async () => { + onDone = jest.fn(); + testBed = await setup({ onDone }); + }); + + it('displays errors', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const invalidPipeline = '{}'; + find('mockCodeEditor').simulate('change', { jsonString: invalidPipeline }); + find('confirmModalConfirmButton').simulate('click'); + const errorCallout = find('loadJsonConfirmationModal.errorCallOut'); + expect(errorCallout.text()).toContain('Please ensure the JSON is a valid pipeline object.'); + expect(onDone).toHaveBeenCalledTimes(0); + }); + + it('passes through a valid pipeline object', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const validPipeline = JSON.stringify({ + processors: [{ set: { field: 'test', value: 123 } }, { badType1: null }, { badType2: 1 }], + on_failure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + find('mockCodeEditor').simulate('change', { jsonString: validPipeline }); + find('confirmModalConfirmButton').simulate('click'); + expect(!exists('loadJsonConfirmationModal')); + expect(onDone).toHaveBeenCalledTimes(1); + expect(onDone.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "on_failure": Array [ + Object { + "gsub": Object { + "field": "_index", + "pattern": "(.monitoring-\\\\w+-)6(-.+)", + "replacement": "$17$2", + }, + }, + ], + "processors": Array [ + Object { + "set": Object { + "field": "test", + "value": 123, + }, + }, + Object { + "badType1": null, + }, + Object { + "badType2": 1, + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx new file mode 100644 index 0000000000000..f183386d5927d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; + +import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; + +import { Processor } from '../../../../../../common/types'; + +import { deserialize } from '../../deserialize'; + +export type OnDoneLoadJsonHandler = (json: { + processors: Processor[]; + on_failure?: Processor[]; +}) => void; + +export interface Props { + onDone: OnDoneLoadJsonHandler; + children: (openModal: () => void) => React.ReactNode; +} + +const i18nTexts = { + modalTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle', { + defaultMessage: 'Load JSON', + }), + buttons: { + cancel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel', { + defaultMessage: 'Cancel', + }), + confirm: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm', { + defaultMessage: 'Load and overwrite', + }), + }, + editor: { + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.editor', { + defaultMessage: 'Pipeline object', + }), + }, + error: { + title: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title', { + defaultMessage: 'Invalid pipeline', + }), + body: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.body', { + defaultMessage: 'Please ensure the JSON is a valid pipeline object.', + }), + }, +}; + +const defaultValue = {}; +const defaultValueRaw = JSON.stringify(defaultValue, null, 2); + +export const ModalProvider: FunctionComponent = ({ onDone, children }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [isValidJson, setIsValidJson] = useState(true); + const [error, setError] = useState(); + const jsonContent = useRef['0']>({ + isValid: true, + validate: () => true, + data: { + format: () => defaultValue, + raw: defaultValueRaw, + }, + }); + const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => { + setIsValidJson(jsonUpdateData.validate()); + jsonContent.current = jsonUpdateData; + }; + return ( + <> + {children(() => setIsModalVisible(true))} + {isModalVisible ? ( + + { + setIsModalVisible(false); + }} + onConfirm={async () => { + try { + const json = jsonContent.current.data.format(); + const { processors, on_failure: onFailure } = json; + // This function will throw if it cannot parse the pipeline object + deserialize({ processors, onFailure }); + onDone(json as any); + setIsModalVisible(false); + } catch (e) { + setError(e); + } + }} + cancelButtonText={i18nTexts.buttons.cancel} + confirmButtonDisabled={!isValidJson} + confirmButtonText={i18nTexts.buttons.confirm} + maxWidth={600} + > +
+ + + + + + + {error && ( + <> + + {i18nTexts.error.body} + + + + )} + + +
+
+
+ ) : undefined} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..c89ff1d3d99ac --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, memo, useMemo } from 'react'; +import { ProcessorsTree } from '.'; +import { usePipelineProcessorsContext } from '../context'; + +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from '../processors_reducer'; + +export interface Props { + stateSlice: typeof ON_FAILURE_STATE_SCOPE | typeof PROCESSOR_STATE_SCOPE; +} + +export const PipelineProcessorsEditor: FunctionComponent = memo( + function PipelineProcessorsEditor({ stateSlice }) { + const { + onTreeAction, + state: { editor, processors }, + } = usePipelineProcessorsContext(); + const baseSelector = useMemo(() => [stateSlice], [stateSlice]); + return ( + + ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx index 5bbea4b89b053..5cee5311c62a9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; interface Props { disabled: boolean; @@ -39,7 +39,7 @@ export const ContextMenu: FunctionComponent = (props) => { onDuplicate(); }} > - {editorItemMessages.duplicateButtonLabel} + {i18nTexts.duplicateButtonLabel} , showAddOnFailure ? ( = (props) => { onAddOnFailure(); }} > - {editorItemMessages.addOnFailureButtonLabel} + {i18nTexts.addOnFailureButtonLabel} ) : undefined, = (props) => { onDelete(); }} > - {editorItemMessages.deleteButtonLabel} + {i18nTexts.deleteButtonLabel} , ].filter(Boolean) as JSX.Element[]; @@ -82,7 +82,7 @@ export const ContextMenu: FunctionComponent = (props) => { disabled={disabled} onClick={() => setIsOpen((v) => !v)} iconType="boxesHorizontal" - aria-label={editorItemMessages.moreButtonAriaLabel} + aria-label={i18nTexts.moreButtonAriaLabel} /> } > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts similarity index 98% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts index 913902d295503..ab080767b6029 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -export const editorItemMessages = { +export const i18nTexts = { moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { defaultMessage: 'Move this processor', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 0fe804adaeb48..09c047d1d51b7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -25,7 +25,7 @@ import './pipeline_processors_editor_item.scss'; import { InlineTextInput } from './inline_text_input'; import { ContextMenu } from './context_menu'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; import { ProcessorInfo } from '../processors_tree'; export interface Handlers { @@ -52,7 +52,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( renderOnFailureHandlers, }) => { const { - state: { editor, processorsDispatch }, + state: { editor, processors }, } = usePipelineProcessorsContext(); const isDisabled = editor.mode.id !== 'idle'; @@ -115,7 +115,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( description: nextDescription, }; } - processorsDispatch({ + processors.dispatch({ type: 'updateProcessor', payload: { processor: { @@ -126,17 +126,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( }, }); }} - ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} + ariaLabel={i18nTexts.processorTypeLabel({ type: processor.type })} text={description} - placeholder={editorItemMessages.descriptionPlaceholder} + placeholder={i18nTexts.descriptionPlaceholder} />
{!isInMoveMode && ( - + { @@ -151,12 +151,12 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( {!isInMoveMode && ( - + @@ -165,7 +165,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( - {editorItemMessages.cancelMoveButtonLabel} + {i18nTexts.cancelMoveButtonLabel}
@@ -183,7 +183,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( editor.setMode({ id: 'removingProcessor', arg: { selector } }); }} onDuplicate={() => { - processorsDispatch({ + processors.dispatch({ type: 'duplicateProcessor', payload: { source: selector, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx index 9d284748a3d15..3eccda55fbb3a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx @@ -9,11 +9,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, memo, useEffect } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiHorizontalRule, EuiFlyout, EuiFlyoutHeader, - EuiTitle, EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; @@ -44,6 +46,11 @@ const addButtonLabel = i18n.translate( { defaultMessage: 'Add' } ); +const cancelButtonLabel = i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + export const ProcessorSettingsForm: FunctionComponent = memo( ({ processor, form, isOnFailure, onClose, onOpen }) => { const { @@ -71,7 +78,7 @@ export const ProcessorSettingsForm: FunctionComponent = memo( return (
- + @@ -109,30 +116,19 @@ export const ProcessorSettingsForm: FunctionComponent = memo( {(arg: any) => { const { type } = arg; - let formContent: React.ReactNode | undefined; if (type?.length) { const formDescriptor = getProcessorFormDescriptor(type as any); if (formDescriptor?.FieldsComponent) { - formContent = ( + return ( <> ); - } else { - formContent = ; } - - return ( - <> - {formContent} - - {processor ? updateButtonLabel : addButtonLabel} - - - ); + return ; } // If the user has not yet defined a type, we do not show any settings fields @@ -140,6 +136,24 @@ export const ProcessorSettingsForm: FunctionComponent = memo( }} + + + + {cancelButtonLabel} + + + { + form.submit(); + }} + > + {processor ? updateButtonLabel : addButtonLabel} + + + + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts index 46e3d1c803fd5..87e6eb7f642a6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './processors_reducer'; + export enum DropSpecialLocations { top = 'TOP', bottom = 'BOTTOM', } + +export const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; +export const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx index fbc06f41208fe..ec864d31d1986 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx @@ -4,41 +4,242 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, Dispatch, FunctionComponent, useContext, useState } from 'react'; -import { EditorMode } from './types'; -import { ProcessorsDispatch } from './processors_reducer'; +import React, { + createContext, + Dispatch, + FunctionComponent, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; + +import { Processor } from '../../../../common/types'; + +import { EditorMode, FormValidityState, OnFormUpdateArg, OnUpdateHandlerArg } from './types'; + +import { + ProcessorsDispatch, + useProcessorsState, + State as ProcessorsState, + isOnFailureSelector, +} from './processors_reducer'; + +import { deserialize } from './deserialize'; + +import { serialize } from './serialize'; + +import { OnSubmitHandler, ProcessorSettingsForm } from './components/processor_settings_form'; + +import { OnActionHandler } from './components/processors_tree'; + +import { ProcessorRemoveModal } from './components'; + +import { getValue } from './utils'; interface Links { esDocsBasePath: string; } -const PipelineProcessorsContext = createContext<{ +interface ContextValue { links: Links; + onTreeAction: OnActionHandler; state: { - processorsDispatch: ProcessorsDispatch; + processors: { + state: ProcessorsState; + dispatch: ProcessorsDispatch; + }; editor: { mode: EditorMode; setMode: Dispatch; }; }; -}>({} as any); +} -interface Props { +const PipelineProcessorsContext = createContext({} as any); + +export interface Props { links: Links; - processorsDispatch: ProcessorsDispatch; + value: { + processors: Processor[]; + onFailure?: Processor[]; + }; + /** + * Give users a way to react to this component opening a flyout + */ + onFlyoutOpen: () => void; + onUpdate: (arg: OnUpdateHandlerArg) => void; } export const PipelineProcessorsContextProvider: FunctionComponent = ({ links, + value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, + onUpdate, + onFlyoutOpen, children, - processorsDispatch, }) => { + const initRef = useRef(false); const [mode, setMode] = useState({ id: 'idle' }); + const deserializedResult = useMemo( + () => + deserialize({ + processors: originalProcessors, + onFailure: originalOnFailureProcessors, + }), + [originalProcessors, originalOnFailureProcessors] + ); + const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + + useEffect(() => { + if (initRef.current) { + processorsDispatch({ + type: 'loadProcessors', + payload: { + newState: deserializedResult, + }, + }); + } else { + initRef.current = true; + } + }, [deserializedResult, processorsDispatch]); + + const { onFailure: onFailureProcessors, processors } = processorsState; + + const [formState, setFormState] = useState({ + validate: () => Promise.resolve(true), + }); + + const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( + ({ isValid, validate }) => { + setFormState({ + validate: async () => { + if (isValid === undefined) { + return validate(); + } + return isValid; + }, + }); + }, + [setFormState] + ); + + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && mode.id === 'idle'; + }, + getData: () => + serialize({ + onFailure: onFailureProcessors, + processors, + }), + }); + }, [processors, onFailureProcessors, onUpdate, formState, mode]); + + const onSubmit = useCallback( + (processorTypeAndOptions) => { + switch (mode.id) { + case 'creatingProcessor': + processorsDispatch({ + type: 'addProcessor', + payload: { + processor: { ...processorTypeAndOptions }, + targetSelector: mode.arg.selector, + }, + }); + break; + case 'editingProcessor': + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...mode.arg.processor, + ...processorTypeAndOptions, + }, + selector: mode.arg.selector, + }, + }); + break; + default: + } + setMode({ id: 'idle' }); + }, + [processorsDispatch, mode, setMode] + ); + + const onCloseSettingsForm = useCallback(() => { + setMode({ id: 'idle' }); + setFormState({ validate: () => Promise.resolve(true) }); + }, [setFormState, setMode]); + + const onTreeAction = useCallback( + (action) => { + switch (action.type) { + case 'addProcessor': + setMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); + break; + case 'move': + setMode({ id: 'idle' }); + processorsDispatch({ + type: 'moveProcessor', + payload: action.payload, + }); + break; + case 'selectToMove': + setMode({ id: 'movingProcessor', arg: action.payload.info }); + break; + case 'cancelMove': + setMode({ id: 'idle' }); + break; + } + }, + [processorsDispatch, setMode] + ); + return ( {children} + + {mode.id === 'editingProcessor' || mode.id === 'creatingProcessor' ? ( + + ) : undefined} + {mode.id === 'removingProcessor' && ( + { + if (confirmed) { + processorsDispatch({ + type: 'removeProcessor', + payload: { selector }, + }); + } + setMode({ id: 'idle' }); + }} + /> + )} ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts new file mode 100644 index 0000000000000..9b7c2069fcddd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.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 { deserialize } from './deserialize'; + +describe('deserialize', () => { + it('tolerates certain bad values correctly', () => { + expect( + deserialize({ + processors: [ + { set: { field: 'test', value: 123 } }, + { badType1: null } as any, + { badType2: 1 } as any, + ], + onFailure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }) + ).toEqual({ + processors: [ + { + id: expect.any(String), + type: 'set', + options: { + field: 'test', + value: 123, + }, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType1', + options: {}, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType2', + options: {}, + }, + ], + onFailure: [ + { + id: expect.any(String), + type: 'gsub', + onFailure: undefined, + options: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + }); + + it('throws for unacceptable values', () => { + expect(() => { + deserialize({ + processors: [{ reallyBad: undefined } as any, 1 as any], + onFailure: [], + }); + }).toThrow('Invalid processor type'); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts index fa1d041bdaba3..1e9a97e189a5e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts @@ -22,12 +22,16 @@ const getProcessorType = (processor: Processor): string => { * See the definition of {@link ProcessorInternal} for why this works to extract the * processor type. */ - return Object.keys(processor)[0]!; + const type: unknown = Object.keys(processor)[0]; + if (typeof type !== 'string') { + throw new Error(`Invalid processor type. Received "${type}"`); + } + return type; }; const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { const type = getProcessorType(processor); - const { on_failure: originalOnFailure, ...options } = processor[type]; + const { on_failure: originalOnFailure, ...options } = processor[type] ?? {}; const onFailure = originalOnFailure?.length ? convertProcessors(originalOnFailure) : (originalOnFailure as ProcessorInternal[] | undefined); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx new file mode 100644 index 0000000000000..7c62383024cfe --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx @@ -0,0 +1,13 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { PipelineProcessorsEditor } from '../components'; + +export const GlobalOnFailureProcessorsEditor: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts new file mode 100644 index 0000000000000..6c544b31df439 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/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 { GlobalOnFailureProcessorsEditor } from './global_on_failure_processors_editor'; +export { ProcessorsEditor } from './processors_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx new file mode 100644 index 0000000000000..108b22be43ca0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx @@ -0,0 +1,13 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { PipelineProcessorsEditor } from '../components'; + +export const ProcessorsEditor: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts index 58d6e492b85e5..89bc50fc0600a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container'; +export { PipelineProcessorsContextProvider, Props } from './context'; -export { OnUpdateHandlerArg } from './types'; +export { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; + +export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; + +export { LoadFromJsonButton, OnDoneLoadJsonHandler } from './components'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx deleted file mode 100644 index 7257677c08fc2..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx +++ /dev/null @@ -1,74 +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, { FunctionComponent, useMemo } from 'react'; - -import { Processor } from '../../../../common/types'; - -import { deserialize } from './deserialize'; - -import { useProcessorsState } from './processors_reducer'; - -import { PipelineProcessorsContextProvider } from './context'; - -import { OnUpdateHandlerArg } from './types'; - -import { PipelineProcessorsEditor as PipelineProcessorsEditorUI } from './pipeline_processors_editor'; - -export interface Props { - value: { - processors: Processor[]; - onFailure?: Processor[]; - }; - onUpdate: (arg: OnUpdateHandlerArg) => void; - isTestButtonDisabled: boolean; - onTestPipelineClick: () => void; - esDocsBasePath: string; - /** - * Give users a way to react to this component opening a flyout - */ - onFlyoutOpen: () => void; -} - -export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; - -export const PipelineProcessorsEditor: FunctionComponent = ({ - value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, - onFlyoutOpen, - onUpdate, - isTestButtonDisabled, - esDocsBasePath, - onTestPipelineClick, -}) => { - const deserializedResult = useMemo( - () => - deserialize({ - processors: originalProcessors, - onFailure: originalOnFailureProcessors, - }), - // TODO: Re-add the dependency on the props and make the state set-able - // when new props come in so that this component will be controllable - [] // eslint-disable-line react-hooks/exhaustive-deps - ); - const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); - const { processors, onFailure } = processorsState; - - return ( - - - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx deleted file mode 100644 index 09e77c5107754..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx +++ /dev/null @@ -1,239 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useCallback, memo, useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch } from '@elastic/eui'; - -import './pipeline_processors_editor.scss'; - -import { - ProcessorsTitleAndTestButton, - OnFailureProcessorsTitle, - ProcessorsTree, - ProcessorRemoveModal, - OnActionHandler, - OnSubmitHandler, - ProcessorSettingsForm, -} from './components'; - -import { ProcessorInternal, OnUpdateHandlerArg, FormValidityState, OnFormUpdateArg } from './types'; - -import { - ON_FAILURE_STATE_SCOPE, - PROCESSOR_STATE_SCOPE, - isOnFailureSelector, -} from './processors_reducer'; - -const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; -const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; - -import { serialize } from './serialize'; -import { getValue } from './utils'; -import { usePipelineProcessorsContext } from './context'; - -export interface Props { - processors: ProcessorInternal[]; - onFailureProcessors: ProcessorInternal[]; - onUpdate: (arg: OnUpdateHandlerArg) => void; - isTestButtonDisabled: boolean; - onTestPipelineClick: () => void; - onFlyoutOpen: () => void; -} - -export const PipelineProcessorsEditor: FunctionComponent = memo( - function PipelineProcessorsEditor({ - processors, - onFailureProcessors, - onTestPipelineClick, - isTestButtonDisabled, - onUpdate, - onFlyoutOpen, - }) { - const { - state: { editor, processorsDispatch }, - } = usePipelineProcessorsContext(); - - const { mode: editorMode, setMode: setEditorMode } = editor; - - const [formState, setFormState] = useState({ - validate: () => Promise.resolve(true), - }); - - const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( - ({ isValid, validate }) => { - setFormState({ - validate: async () => { - if (isValid === undefined) { - return validate(); - } - return isValid; - }, - }); - }, - [setFormState] - ); - - const [showGlobalOnFailure, setShowGlobalOnFailure] = useState( - Boolean(onFailureProcessors.length) - ); - - useEffect(() => { - onUpdate({ - validate: async () => { - const formValid = await formState.validate(); - return formValid && editorMode.id === 'idle'; - }, - getData: () => - serialize({ - onFailure: showGlobalOnFailure ? onFailureProcessors : undefined, - processors, - }), - }); - }, [processors, onFailureProcessors, onUpdate, formState, editorMode, showGlobalOnFailure]); - - const onSubmit = useCallback( - (processorTypeAndOptions) => { - switch (editorMode.id) { - case 'creatingProcessor': - processorsDispatch({ - type: 'addProcessor', - payload: { - processor: { ...processorTypeAndOptions }, - targetSelector: editorMode.arg.selector, - }, - }); - break; - case 'editingProcessor': - processorsDispatch({ - type: 'updateProcessor', - payload: { - processor: { - ...editorMode.arg.processor, - ...processorTypeAndOptions, - }, - selector: editorMode.arg.selector, - }, - }); - break; - default: - } - setEditorMode({ id: 'idle' }); - }, - [processorsDispatch, editorMode, setEditorMode] - ); - - const onCloseSettingsForm = useCallback(() => { - setEditorMode({ id: 'idle' }); - setFormState({ validate: () => Promise.resolve(true) }); - }, [setFormState, setEditorMode]); - - const onTreeAction = useCallback( - (action) => { - switch (action.type) { - case 'addProcessor': - setEditorMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); - break; - case 'move': - setEditorMode({ id: 'idle' }); - processorsDispatch({ - type: 'moveProcessor', - payload: action.payload, - }); - break; - case 'selectToMove': - setEditorMode({ id: 'movingProcessor', arg: action.payload.info }); - break; - case 'cancelMove': - setEditorMode({ id: 'idle' }); - break; - } - }, - [processorsDispatch, setEditorMode] - ); - - const movingProcessor = editorMode.id === 'movingProcessor' ? editorMode.arg : undefined; - - return ( -
- - - - - - - - - - - - - - - - } - checked={showGlobalOnFailure} - onChange={(e) => setShowGlobalOnFailure(e.target.checked)} - data-test-subj="pipelineEditorOnFailureToggle" - /> - - {showGlobalOnFailure ? ( - - - - ) : undefined} - - {editorMode.id === 'editingProcessor' || editorMode.id === 'creatingProcessor' ? ( - - ) : undefined} - {editorMode.id === 'removingProcessor' && ( - { - if (confirmed) { - processorsDispatch({ - type: 'removeProcessor', - payload: { selector }, - }); - } - setEditorMode({ id: 'idle' }); - }} - /> - )} -
- ); - } -); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts index 7265f63f45a5d..0e06b8d55d379 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts @@ -12,6 +12,6 @@ export { Action, } from './processors_reducer'; -export { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './constants'; +export * from './constants'; export { isChildPath, isOnFailureSelector } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts index 4e069aab8bdd1..295e7ff141117 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts @@ -38,6 +38,12 @@ export type Action = payload: { source: ProcessorSelector; }; + } + | { + type: 'loadProcessors'; + payload: { + newState: DeserializeResult; + }; }; export type ProcessorsDispatch = Dispatch; @@ -124,6 +130,14 @@ export const reducer: Reducer = (state, action) => { return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray); } + if (action.type === 'loadProcessors') { + return { + ...action.payload.newState, + onFailure: action.payload.newState.onFailure ?? [], + isRoot: true, + }; + } + return state; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts index aa39fca29fa8b..aea8f0f0910f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -38,6 +38,8 @@ export interface OnUpdateHandlerArg extends FormValidityState { getData: () => SerializeResult; } +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + /** * The editor can be in different modes. This enables us to hold * a reference to data dispatch to the reducer (like the {@link ProcessorSelector} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 05e7d1e41c5fa..d2c4b73a48767 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -22,6 +22,8 @@ export { UseRequestConfig, WithPrivileges, Monaco, + JsonEditor, + OnJsonEditorUpdateHandler, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6064e044015b7..f7f2f4a44ee11 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8478,13 +8478,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名前", "xpack.ingestPipelines.form.nameTitle": "名前", "xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー", - "xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。", - "xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。", "xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存", "xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません", "xpack.ingestPipelines.form.savingButtonLabel": "保存中…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed57e2061e32e..1917a22c2e8f8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8483,13 +8483,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名称", "xpack.ingestPipelines.form.nameTitle": "名称", "xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "处理器", - "xpack.ingestPipelines.form.processorsJsonError": "输入无效。", - "xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。", "xpack.ingestPipelines.form.saveButtonLabel": "保存管道", "xpack.ingestPipelines.form.savePipelineError": "无法创建管道", "xpack.ingestPipelines.form.savingButtonLabel": "正在保存......", From a999009bbd26b999fd6bd0a2adf85aadacc25b46 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sun, 5 Jul 2020 23:00:13 +0100 Subject: [PATCH 11/12] skip flaky suite (#70762) --- .../src/integration_tests/basic_optimization.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index b2e5f6257669c..44c9a58b35068 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -63,7 +63,8 @@ afterAll(async () => { await del(TMP_DIR); }); -it('builds expected bundles, saves bundle counts to metadata', async () => { +// FLAKY: https://github.com/elastic/kibana/issues/70762 +it.skip('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], From 36c5490b0150a63233706f53b1e85b18eae90d47 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Jul 2020 00:55:02 +0100 Subject: [PATCH 12/12] skip flaky suite (#70764) --- .../src/integration_tests/basic_optimization.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 44c9a58b35068..109bb7df8cc18 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -168,7 +168,8 @@ it.skip('builds expected bundles, saves bundle counts to metadata', async () => `); }); -it('uses cache on second run and exist cleanly', async () => { +// FLAKY: https://github.com/elastic/kibana/issues/70764 +it.skip('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')],